// 拳皇97 · 双人对战（大蛇 vs 草薙京）- 简化还原 + BO3 + 皮肤 + 语音/SFX + 雪碧图支持 + 自动/手动原版皮肤导入 + 帧动画/防御
(function(){
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  let W=0,H=0, GROUND=0;
  function resize(){
    const vw = Math.max(960, window.innerWidth);
    const vh = Math.max(540, window.innerHeight);
    canvas.width = vw; canvas.height = vh; W = vw; H = vh; GROUND = Math.floor(H*0.82);
  }
  window.addEventListener('resize', resize); resize();

  // 顶部导入面板交互（可选）
  const importModal = document.getElementById('import-modal');
  const btnImport = document.getElementById('btn-import');
  const btnImportApply = document.getElementById('btn-import-apply');
  const btnImportClose = document.getElementById('btn-import-close');
  const btnImportPresetTry = document.getElementById('btn-import-preset-try');
  if(btnImport){ btnImport.addEventListener('click',()=>{ importModal.style.display='block'; }); }
  if(btnImportClose){ btnImportClose.addEventListener('click',()=>{ importModal.style.display='none'; }); }

  // 工具
  const now = () => performance.now();
  const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
  const sign = (v)=> (v<0?-1:(v>0?1:0));

  // 颜色
  const C = {
    bgTop:'#11131a', bgBottom:'#0b0e14', crowd:'#1b1f2a', floor:'#151821',
    hpGood:'#6cf088', hpWarn:'#f7c948', hpBad:'#ff6b6b', bar:'#0f1219',
    text:'#e6edf3'
  };

  // 角色皮肤（调色板）
  const SKINS = {
    kyo: [
      { c1:'#f0642a', c2:'#ffb36b' },
      { c1:'#2a78f0', c2:'#6bb7ff' },
      { c1:'#1f1f1f', c2:'#ff4e4e' },
      { c1:'#0c4a6e', c2:'#98c5ff' },
    ],
    orochi: [
      { c1:'#8e7aff', c2:'#bfa8ff' },
      { c1:'#a8e3ff', c2:'#eaffff' },
      { c1:'#2b203f', c2:'#b48cff' },
      { c1:'#e3c851', c2:'#fff4ad' },
    ]
  };

  // 原版皮肤导入管理（localStorage 缓存）
  const ImportManager = (function(){
    const prefix = 'kof97:';
    const store = {};
    const keys = ['kyo_idle','kyo_sheet','orochi_idle','orochi_sheet'];
    function load(){ keys.forEach(k=>{ const v = localStorage.getItem(prefix+k); if(v) store[k]=v; }); }
    function get(k){ return store[k]||null; }
    function set(k, dataURL){ store[k]=dataURL; try{ localStorage.setItem(prefix+k, dataURL); }catch(e){} }
    function fromBlob(blob){ return new Promise((resolve)=>{ const fr=new FileReader(); fr.onload=()=>resolve(fr.result); fr.readAsDataURL(blob); }); }
    async function fromFile(file){ return fromBlob(file); }
    async function fromUrl(url){ const resp = await fetch(url, {mode:'cors'}); const ct=(resp.headers.get('content-type')||''); if(!ct.includes('image')) throw new Error('not_image'); const blob = await resp.blob(); return fromBlob(blob); }
    // 具备代理回退的智能抓取（提高跨域成功率）
    async function fromUrlSmart(url){
      const proxies = (u)=>[
        u,
        `https://images.weserv.nl/?url=${encodeURIComponent(u)}`,
        `https://cors.isomorphic-git.org/${u}`
      ];
      const isImageLike = /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/i.test(url);
      // 直链或代理直抓（仅在看起来像图片链接时尝试）
      if(isImageLike){
        for(const u of proxies(url)){ try{ const data = await fromUrl(u); if(data) return data; }catch(e){} }
      }
      // 解析页面提取候选图片，含页面级代理
      const fetchHtmlWithFallback = async (u)=>{
        const toJina = (src)=>{
          try{
            if(src.startsWith('https://')) return `https://r.jina.ai/http://${src.slice(8)}`;
            if(src.startsWith('http://')) return `https://r.jina.ai/${src}`;
          }catch(e){}
          return `https://r.jina.ai/http://${src}`;
        };
        const list = [u, `https://cors.isomorphic-git.org/${u}`, toJina(u)];
        for(const x of list){ try{ const r = await fetch(x, {mode:'cors'}); return r; }catch(e){} }
        return null;
      };
      const resp = await fetchHtmlWithFallback(url);
      if(!resp) throw new Error('fetch_failed');
      const ct = (resp.headers.get('content-type')||'');
      if(ct.includes('image')){ const blob = await resp.blob(); return fromBlob(blob); }
      const html = await resp.text();
      const doc = new DOMParser().parseFromString(html, 'text/html');
      const candidates = [];
      const og = doc.querySelector('meta[property="og:image"]')?.getAttribute('content');
      const tw = doc.querySelector('meta[name="twitter:image"]')?.getAttribute('content');
      if(og){ candidates.push(new URL(og, url).href); }
      if(tw){ candidates.push(new URL(tw, url).href); }
      doc.querySelectorAll('img').forEach(img=>{
        const s = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original');
        if(s){ candidates.push(new URL(s, url).href); }
      });
      for(const cand of candidates){ for(const u of proxies(cand)){ try{ const data = await fromUrl(u); if(data) return data; }catch(e){} } }
      throw new Error('no_image_found');
    }
    load();
    function clear(){ keys.forEach(k=>{ delete store[k]; try{ localStorage.removeItem(prefix+k); }catch(e){} }); }
    return { get, set, fromFile, fromUrl, fromUrlSmart, keys, clear };
  })();

  // 自动导入：更贴近原版风格的像素standing图（无需手动）
  const AutoImport = (function(){
    function drawKyoIdle(){
      const w=64,h=80; const c=document.createElement('canvas'); c.width=w; c.height=h; const g=c.getContext('2d'); g.imageSmoothingEnabled=false;
      g.clearRect(0,0,w,h);
      g.fillStyle='#0e0e10'; g.fillRect(26,22,12,32);
      g.fillStyle='#20232a'; g.fillRect(27,23,10,30);
      g.fillStyle='#3b3f47'; g.fillRect(28,24,8,12);
      g.fillStyle='#ffffff'; g.fillRect(22,34,4,4); g.fillRect(40,34,4,4);
      g.fillStyle='#2a2d33'; g.fillRect(22,31,4,3); g.fillRect(40,31,4,3);
      g.fillStyle='#d8c195'; g.fillRect(30,14,6,7);
      g.fillStyle='#6d3f1f'; g.fillRect(29,12,8,4);
      g.fillStyle='#ffffff'; g.fillRect(28,19,10,2);
      g.fillStyle='#fafafa'; g.fillRect(28,24,8,2);
      g.fillStyle='#ffffff'; g.fillRect(31,28,3,3);
      g.fillStyle='#d8c195'; g.fillRect(22,26,4,8); g.fillRect(40,26,4,8);
      g.fillStyle='#333842'; g.fillRect(27,46,10,3);
      g.fillStyle='#15181f'; g.fillRect(27,50,5,20); g.fillRect(33,50,5,20);
      g.fillStyle='#2b2f36'; g.fillRect(27,68,5,6); g.fillRect(33,68,5,6);
      return c.toDataURL('image/png');
    }
    function drawOrochiIdle(){
      const w=72,h=88; const c=document.createElement('canvas'); c.width=w; c.height=h; const g=c.getContext('2d'); g.imageSmoothingEnabled=false;
      g.clearRect(0,0,w,h);
      g.fillStyle='#221b35'; g.fillRect(30,26,12,36);
      g.fillStyle='#3b2e5a'; g.fillRect(31,27,10,34);
      g.fillStyle='#5a4791'; g.fillRect(26,24,20,18);
      g.fillStyle='#8e7aff'; g.fillRect(28,20,16,6);
      g.fillStyle='#eae6ff'; g.fillRect(33,12,6,7);
      g.fillStyle='#dcd8ff'; g.fillRect(31,10,10,4);
      g.fillStyle='#eae6ff'; g.fillRect(26,28,4,10); g.fillRect(42,28,4,10);
      g.fillStyle='#2b203f'; g.fillRect(31,62,5,18); g.fillRect(37,62,5,18);
      g.fillStyle='#7c69cc'; g.fillRect(31,80,5,6); g.fillRect(37,80,5,6);
      g.fillStyle='#bfa8ff'; g.fillRect(35,34,3,3);
      return c.toDataURL('image/png');
    }
    function makeSheet(frameW, frameH, cols, rows, drawFn){
      const c=document.createElement('canvas'); c.width=frameW*cols; c.height=frameH*rows; const g=c.getContext('2d'); g.imageSmoothingEnabled=false;
      for(let i=0;i<cols*rows;i++){
        const col=i%cols, row=Math.floor(i/cols);
        g.save(); g.translate(col*frameW, row*frameH);
        g.clearRect(0,0,frameW,frameH);
        drawFn(g, i, frameW, frameH);
        g.restore();
      }
      return c.toDataURL('image/png');
    }
    function drawKyoFrame(g, i, fw, fh){
      const baseX=fw/2, baseY=fh*0.9;
      const phase=i%8; const row=Math.floor(i/8);
      const dx = (row===0? (phase-4)*0.6 : 0);
      const up = (row===1? -10 : 0);
      // torso
      g.fillStyle='#0e0e10'; g.fillRect(baseX-6+dx, baseY-58+up, 12, 32);
      g.fillStyle='#20232a'; g.fillRect(baseX-5+dx, baseY-57+up, 10, 30);
      g.fillStyle='#3b3f47'; g.fillRect(baseX-4+dx, baseY-56+up, 8, 12);
      // head + band
      g.fillStyle='#d8c195'; g.fillRect(baseX-3+dx, baseY-72+up, 6, 7);
      g.fillStyle='#6d3f1f'; g.fillRect(baseX-4+dx, baseY-76+up, 8, 4);
      g.fillStyle='#ffffff'; g.fillRect(baseX-5+dx, baseY-65+up, 10, 2);
      // arms swing or attack
      const isAttack = (row===2 && (i%8)>=0) || (row===3) || (row===4);
      const armLen = isAttack? 18 : (row===0? (8+(phase%4)) : 10);
      g.fillStyle='#d8c195'; g.fillRect(baseX-14+dx, baseY-52+up, 4, 8);
      g.fillRect(baseX+10+dx, baseY-52+up, 4, 8);
      g.fillStyle='#ffffff'; g.fillRect(baseX-14+dx, baseY-44+up, 4, 4); g.fillRect(baseX+10+dx, baseY-44+up, 4, 4);
      g.fillStyle='#20232a'; g.fillRect(baseX-16+dx, baseY-44+up, 6, armLen);
      g.fillRect(baseX+8+dx, baseY-44+up, 6, armLen);
      // belt
      g.fillStyle='#333842'; g.fillRect(baseX-5+dx, baseY-30+up, 10, 3);
      // legs
      const crouch = (row===5 && i===43);
      const legH = crouch? 12 : 20;
      g.fillStyle='#15181f'; g.fillRect(baseX-5+dx, baseY-26+up, 5, legH);
      g.fillRect(baseX+1+dx, baseY-26+up, 5, legH);
      g.fillStyle='#2b2f36'; g.fillRect(baseX-5+dx, baseY-6+up, 5, 6);
      g.fillRect(baseX+1+dx, baseY-6+up, 5, 6);
      // effects for special/super
      if(row===3){ g.globalAlpha=0.75; g.fillStyle='#ff9b4a'; g.fillRect(baseX+24, baseY-60, 30, 10); }
      if(row===4){ g.globalAlpha=0.85; g.fillStyle='#ffd56b'; g.fillRect(baseX+24, baseY-72, 50, 14); }
      // hit pose
      if(row===5 && (i===40||i===41)){ g.globalAlpha=0.6; g.fillStyle='#ff6b6b'; g.fillRect(baseX-24, baseY-80, 50, 8); }
      // guard pose
      if(row===5 && i===42){ g.globalAlpha=0.7; g.strokeStyle='#a0b7ff'; g.lineWidth=2; g.strokeRect(baseX-26, baseY-86, 52, 84); }
    }
    function drawOrochiFrame(g, i, fw, fh){
      const baseX=fw/2, baseY=fh*0.92; const phase=i%8; const row=Math.floor(i/8);
      const dx=(row===0? (phase-4)*0.6 : 0); const up=(row===1? -12 : 0);
      // cloak + torso
      g.fillStyle='#221b35'; g.fillRect(baseX-6+dx, baseY-62+up, 12, 36);
      g.fillStyle='#3b2e5a'; g.fillRect(baseX-5+dx, baseY-61+up, 10, 34);
      g.fillStyle='#5a4791'; g.fillRect(baseX-10+dx, baseY-64+up, 20, 18);
      g.fillStyle='#8e7aff'; g.fillRect(baseX-8+dx, baseY-68+up, 16, 6);
      // head
      g.fillStyle='#eae6ff'; g.fillRect(baseX-3+dx, baseY-78+up, 6, 7);
      g.fillStyle='#dcd8ff'; g.fillRect(baseX-4+dx, baseY-82+up, 10, 4);
      // arms
      const armLen = (row===2||row===3||row===4)? 18 : 10;
      g.fillStyle='#eae6ff'; g.fillRect(baseX-14+dx, baseY-56+up, 4, 10); g.fillRect(baseX+10+dx, baseY-56+up, 4, 10);
      g.fillStyle='#3b2e5a'; g.fillRect(baseX-16+dx, baseY-46+up, 6, armLen); g.fillRect(baseX+8+dx, baseY-46+up, 6, armLen);
      // legs
      const crouch=(row===5 && i===43); const lh=crouch? 12:18;
      g.fillStyle='#2b203f'; g.fillRect(baseX-5+dx, baseY-30+up, 5, lh); g.fillRect(baseX+1+dx, baseY-30+up, 5, lh);
      g.fillStyle='#7c69cc'; g.fillRect(baseX-5+dx, baseY-12+up, 5, 6); g.fillRect(baseX+1+dx, baseY-12+up, 5, 6);
      // emblem
      g.fillStyle='#bfa8ff'; g.fillRect(baseX-2+dx, baseY-48+up, 3, 3);
      // special/super
      if(row===3){ g.globalAlpha=0.75; g.fillStyle='#a88cff'; g.fillRect(baseX+24, baseY-60, 30, 10); }
      if(row===4){ g.globalAlpha=0.85; g.fillStyle='#cbb7ff'; g.fillRect(baseX+24, baseY-72, 50, 14); }
      // hit
      if(row===5 && (i===40||i===41)){ g.globalAlpha=0.6; g.fillStyle='#ff6b6b'; g.fillRect(baseX-24, baseY-80, 50, 8); }
      // guard
      if(row===5 && i===42){ g.globalAlpha=0.7; g.strokeStyle='#a0b7ff'; g.lineWidth=2; g.strokeRect(baseX-26, baseY-86, 52, 84); }
    }
    function seedOnce(){
      let seeded=false;
      if(!ImportManager.get('kyo_idle')){ ImportManager.set('kyo_idle', drawKyoIdle()); seeded=true; }
      if(!ImportManager.get('orochi_idle')){ ImportManager.set('orochi_idle', drawOrochiIdle()); seeded=true; }
      if(!ImportManager.get('kyo_sheet')){ const data= makeSheet(64,80,8,6, drawKyoFrame); ImportManager.set('kyo_sheet', data); seeded=true; }
      if(!ImportManager.get('orochi_sheet')){ const data= makeSheet(72,88,8,6, drawOrochiFrame); ImportManager.set('orochi_sheet', data); seeded=true; }
      return seeded;
    }
    async function removeBgSmart(dataUrl){
      function loadImage(src){ return new Promise((res,rej)=>{ const img=new Image(); img.crossOrigin='anonymous'; img.onload=()=>res(img); img.onerror=rej; img.src=src; }); }
      const img = await loadImage(dataUrl);
      const w = img.width, h = img.height;
      const c = document.createElement('canvas'); c.width=w; c.height=h; const g=c.getContext('2d');
      g.drawImage(img,0,0,w,h);
      const id = g.getImageData(0,0,w,h); const a = id.data;
      function sample(x0,y0,dx,dy){ let r=0,g=0,b=0,n=0; for(let y=y0;y<y0+dy;y++){ for(let x=x0;x<x0+dx;x++){ const i=(y*w+x)*4; r+=a[i]; g+=a[i+1]; b+=a[i+2]; n++; } } return [r/n,g/n,b/n]; }
      const sTL = sample(0,0,Math.max(4,Math.floor(w*0.03)), Math.max(4,Math.floor(h*0.03)));
      const sTR = sample(w-Math.max(4,Math.floor(w*0.03)),0, Math.max(4,Math.floor(w*0.03)), Math.max(4,Math.floor(h*0.03)));
      const sBL = sample(0,h-Math.max(4,Math.floor(h*0.03)), Math.max(4,Math.floor(w*0.03)), Math.max(4,Math.floor(h*0.03)));
      const sBR = sample(w-Math.max(4,Math.floor(w*0.03)),h-Math.max(4,Math.floor(h*0.03)), Math.max(4,Math.floor(w*0.03)), Math.max(4,Math.floor(h*0.03)));
      const samples = [sTL,sTR,sBL,sBR];
      const avgBright = samples.reduce((s,[r,g,b])=> s+(r+g+b)/3,0)/samples.length;
      let thrBg = 34, thrEdge = 72;
      if(avgBright>200){ thrBg=52; thrEdge=96; } else if(avgBright<40){ thrBg=46; thrEdge=88; }
      function dist(p, q){ return Math.abs(p[0]-q[0])+Math.abs(p[1]-q[1])+Math.abs(p[2]-q[2]); }
      // Pass1: alpha by distance to background samples
      for(let y=0;y<h;y++){
        for(let x=0;x<w;x++){
          const i=(y*w+x)*4; const p=[a[i],a[i+1],a[i+2]];
          let dMin=9999; for(const s of samples){ const d=dist(p,s); if(d<dMin) dMin=d; }
          if(dMin<=thrBg){ a[i+3]=0; }
          else if(dMin<=thrEdge){ const t=(dMin-thrBg)/(thrEdge-thrBg); a[i+3]=Math.floor(255*(1-t)); }
          else { a[i+3]=255; }
        }
      }
      // Pass2: light feathering
      const out = new Uint8ClampedArray(a.length); out.set(a);
      function alphaAt(x,y){ if(x<0||y<0||x>=w||y>=h) return 0; return a[(y*w+x)*4+3]; }
      for(let y=0;y<h;y++){
        for(let x=0;x<w;x++){
          const i=(y*w+x)*4; const aa = a[i+3]; if(aa===0){ out[i+3]=0; continue; }
          const sum = alphaAt(x-1,y)+alphaAt(x+1,y)+alphaAt(x,y-1)+alphaAt(x,y+1)+alphaAt(x-1,y-1)+alphaAt(x+1,y-1)+alphaAt(x-1,y+1)+alphaAt(x+1,y+1);
          const avg = sum/8; out[i+3] = Math.max(aa, avg*0.9);
        }
      }
      id.data.set(out); g.putImageData(id,0,0);
      return c.toDataURL('image/png');
    }
    async function augmentLegsIfNeededKyo(dataUrl){
      function loadImage(src){ return new Promise((res,rej)=>{ const img=new Image(); img.crossOrigin='anonymous'; img.onload=()=>res(img); img.onerror=rej; img.src=src; }); }
      const img = await loadImage(dataUrl);
      const w = img.width, h = img.height;
      const c = document.createElement('canvas'); c.width=w; c.height=h; const g=c.getContext('2d');
      g.clearRect(0,0,w,h); g.drawImage(img,0,0,w,h);
      const id = g.getImageData(0,0,w,h); const a = id.data;
      let contentBottom = Math.floor(h*0.99);
      for(let y=h-1;y>=0;y--){ let alphaSum=0; for(let x=0;x<w;x++){ alphaSum += a[(y*w+x)*4+3]; } if(alphaSum> w*3){ contentBottom = y; break; } }
      if(contentBottom > Math.floor(h*0.78)) return c.toDataURL('image/png');
      const legLen = Math.min(Math.floor(h*0.28), h - contentBottom - 2);
      const midX = Math.floor(w/2);
      const legW = Math.max(Math.floor(w*0.14), 16);
      const gap = Math.max(Math.floor(w*0.08), 10);
      const startY = contentBottom;
      const colorPant = '#2b2f36'; const colorPantShadow = '#15181f'; const colorShoe = '#0c0f14';
      g.save(); g.globalAlpha = 0.98;
      g.fillStyle = colorPant; g.fillRect(midX - gap/2 - legW, startY, legW, legLen);
      g.fillStyle = colorPantShadow; g.fillRect(midX - gap/2 - legW, startY + legLen - Math.max(6, Math.floor(legLen*0.2)), legW, Math.max(6, Math.floor(legLen*0.2)));
      g.fillStyle = colorPant; g.fillRect(midX + gap/2, startY, legW, legLen);
      g.fillStyle = colorPantShadow; g.fillRect(midX + gap/2, startY + legLen - Math.max(6, Math.floor(legLen*0.2)), legW, Math.max(6, Math.floor(legLen*0.2)));
      const shoeH = Math.max(6, Math.floor(h*0.04));
      g.fillStyle = colorShoe; g.fillRect(midX - gap/2 - legW, startY + legLen, legW, shoeH); g.fillRect(midX + gap/2, startY + legLen, legW, shoeH);
      g.restore();
      return c.toDataURL('image/png');
    }
    async function generateSheetFromImage(dataUrl, frameW, frameH, cols, rows){
      function loadImage(src){ return new Promise((res,rej)=>{ const img=new Image(); img.crossOrigin='anonymous'; img.onload=()=>res(img); img.onerror=rej; img.src=src; }); }
      const cleaned = await removeBgSmart(dataUrl);
      const baseImg = await loadImage(cleaned);
      const c=document.createElement('canvas'); c.width=frameW*cols; c.height=frameH*rows; const g=c.getContext('2d'); g.imageSmoothingEnabled=false;
      for(let i=0;i<cols*rows;i++){
        const col=i%cols, row=Math.floor(i/cols); const phase=i%8;
        g.save(); g.translate(col*frameW, row*frameH);
        g.clearRect(0,0,frameW,frameH);
        let dx=0, dy=0, rot=0, scale=1;
        if(row===0){ dx=(phase-4)*0.6; }
        else if(row===1){ dy=-10 + ((phase%3===0)? -2:0); }
        else if(row===2){ rot=((phase-4)*0.02); dx=(phase%2?1:-1)*2; }
        else if(row===3){ dx=2; dy=-4; }
        else if(row===4){ dy=-6; scale=1.02; }
        else if(row===5){ dy=4; }
        const iw = baseImg.width; const ih = baseImg.height;
        const s = Math.min(frameW*0.9/iw, frameH*0.9/ih);
        g.translate(frameW/2, frameH/2);
        g.rotate(rot);
        g.drawImage(baseImg, -iw*s/2+dx, -ih*s/2+dy, iw*s*scale, ih*s*scale);
        g.restore();
      }
      return c.toDataURL('image/png');
    }
    return { seedOnce, generateSheetFromImage, removeBgSmart, augmentLegsIfNeededKyo };
  })();

  // 雪碧图引擎 + 帧动画映射
  const SpriteEngine = (function(){
     const cfg = {
       kyo: {
         idle: 'assets/kof97/kyo_idle.png',
         sheet: 'assets/kof97/kyo_sheet.png',
         frameW: 64, frameH: 80, cols: 8, rows: 6,
         scale: 2.6,
         atlas: {
           idle: { frames:[0], rate:220 },
           walk: { frames:[0,1,2,3,4,5,6,7,6,5,4,3,2,1,0], rate:90 },
           run: { frames:[0,1,2,3,4,5,6,7], rate:85 },
           jump: { frames:[8,9,10,11,12,13,14,15], rate:85 },
           attack: { frames:[16,17,18,19,20,21,22,23], rate:75 },
           throw: { frames:[16,17,18,19], rate:80 },
           roll: { frames:[42], rate:220 },
           special: { frames:[24,25,26,27], rate:80 },
           super: { frames:[32,33,34,35,36,37], rate:70 },
           hit: { frames:[40,41], rate:120 },
           guard: { frames:[40,41,42,43,42,41], rate:180 },
           crouch: { frames:[43], rate:220 }
         }
       },
       orochi: {
         idle: 'assets/kof97/orochi_idle.png',
         sheet: 'assets/kof97/orochi_sheet.png',
         frameW: 72, frameH: 88, cols: 8, rows: 6,
         scale: 2.6,
         atlas: {
           idle: { frames:[0], rate:220 },
           walk: { frames:[0,1,2,3,4,5,6,7,6,5,4,3,2,1,0], rate:90 },
           run: { frames:[0,1,2,3,4,5,6,7], rate:85 },
           jump: { frames:[8,9,10,11,12,13,14,15], rate:85 },
           attack: { frames:[16,17,18,19,20,21,22,23], rate:75 },
           throw: { frames:[16,17,18,19], rate:80 },
           roll: { frames:[42], rate:220 },
           special: { frames:[24,25,26,27], rate:80 },
           super: { frames:[32,33,34,35,36,37], rate:70 },
           hit: { frames:[40,41], rate:120 },
           guard: { frames:[40,41,42,43,42,41], rate:180 },
           crouch: { frames:[43], rate:220 }
         }
       }
     };
     const cache = {};
     function loadImage(src){ return new Promise((resolve,reject)=>{ const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = ()=>resolve(img); img.onerror = reject; img.src = src; }); }
     async function ensure(name){
       if(cache[name]) return cache[name];
       const c = cfg[name]; const out = { idle:null, sheet:null, conf:c };
       // 优先加载 assets 下的原版皮肤
       try{ out.idle = await loadImage(c.idle); }catch(e){}
       try{ out.sheet = await loadImage(c.sheet); }catch(e){}
       // 如 assets 缺失，则回退到用户导入/缓存的资源
       if(!out.idle){ const altIdle = ImportManager.get(name==='kyo'?'kyo_idle':'orochi_idle'); try{ if(altIdle) out.idle = await loadImage(altIdle); }catch(e){} }
       if(!out.sheet){ const altSheet = ImportManager.get(name==='kyo'?'kyo_sheet':'orochi_sheet'); try{ if(altSheet) out.sheet = await loadImage(altSheet); }catch(e){} }
       cache[name] = out; return out;
     }
     function draw(name, x, y, face, state, animIdx){
      const s = cache[name]; if(!s) return false;
      const scale = s.conf.scale || 2.4;
      ctx.save(); ctx.translate(x, y); ctx.scale(face,1);
      if(window.forceVector===true){ ctx.restore(); return false; }
      // 高清平滑与阴影
      ctx.imageSmoothingEnabled = (window.spriteSmooth!==false);
      try{ ctx.imageSmoothingQuality = 'high'; }catch(e){}
      if(window.spriteShadow!==false){ ctx.shadowColor='rgba(0,0,0,0.35)'; ctx.shadowBlur=8; ctx.shadowOffsetX=0; ctx.shadowOffsetY=4; } else { ctx.shadowBlur=0; ctx.shadowColor='transparent'; }
      // 优先用sheet + atlas
      if(s.sheet && s.conf.atlas){
        const atlas = s.conf.atlas[state] || s.conf.atlas.idle;
        const frameIdx = atlas.frames[animIdx % atlas.frames.length] || 0;
        const cols = s.conf.cols || 8, rows = s.conf.rows || 6;
        let fw = s.conf.frameW, fh = s.conf.frameH;
        // 自动从高清雪碧图计算帧尺寸（与配置不一致时以实际图像为准）
        if(s.sheet && s.sheet.width && s.sheet.height){
          const fwCalc = Math.floor(s.sheet.width / cols);
          const fhCalc = Math.floor(s.sheet.height / rows);
          if(fwCalc * cols === s.sheet.width && fhCalc * rows === s.sheet.height){ fw = fwCalc; fh = fhCalc; }
        }
        const sx = (frameIdx % cols) * fw;
        const sy = Math.floor(frameIdx / cols) * fh;
        // 保持屏幕显示高度稳定：按配置的基础高度缩放
        const baseFH = s.conf.frameH || fh;
        const targetH = baseFH * scale;
        const targetW = fw * (targetH / fh);
        try{ ctx.drawImage(s.sheet, sx, sy, fw, fh, -targetW/2, -targetH, targetW, targetH); ctx.restore(); return true; }catch(e){}
      }
      // 回退：使用idle单图（按目标高度等比缩放）
      if(s.idle){
        const iw = s.idle.width, ih = s.idle.height;
        const baseFH = s.conf.frameH || ih;
        const targetH = baseFH * scale;
        const targetW = iw * (targetH / ih);
        try{ ctx.drawImage(s.idle, -targetW/2, -targetH, targetW, targetH); ctx.restore(); return true; }catch(e){}
      }
      ctx.restore(); return false;
    }
    function clear(){ Object.keys(cache).forEach(k=>{ delete cache[k]; }); }
    return { ensure, draw, clear, cfg };
  })();

  // 音频引擎（WebAudio + 语音）
  const AudioEngine = (function(){
    let ctxA=null, master=null; let voiceEnabled=true;
    function ensure(){ if(!ctxA){ ctxA = new (window.AudioContext||window.webkitAudioContext)(); master = ctxA.createGain(); master.gain.value=0.7; master.connect(ctxA.destination);} }
    function env(node, t=0.15){ const g = ctxA.createGain(); g.gain.setValueAtTime(0, ctxA.currentTime); g.gain.linearRampToValueAtTime(1, ctxA.currentTime+0.01); g.gain.exponentialRampToValueAtTime(0.001, ctxA.currentTime+t); node.connect(g); g.connect(master); setTimeout(()=>{try{node.disconnect(); g.disconnect();}catch(e){}} , t*1000+100); }
    function osc(type, freq, t){ const o = ctxA.createOscillator(); o.type=type; o.frequency.value=freq; o.start(); env(o, t); o.stop(ctxA.currentTime+t); }
    function noise(t=0.12, color='white'){ const s = ctxA.createBufferSource(); const buffer = ctxA.createBuffer(1, ctxA.sampleRate*t, ctxA.sampleRate); const data=buffer.getChannelData(0); for(let i=0;i<data.length;i++){ const n = Math.random()*2-1; data[i]= color==='white'? n : n*(i/data.length); } s.buffer=buffer; s.loop=false; env(s, t); s.start(); }
    function sfx(name){ try{ ensure(); switch(name){
      case 'hit': noise(0.08); osc('square', 160, 0.06); break;
      case 'heavy': noise(0.12); osc('sawtooth', 120, 0.12); break;
      case 'jump': osc('triangle', 420, 0.08); break;
      case 'land': osc('triangle', 180, 0.06); noise(0.03); break;
      case 'qcf': osc('sawtooth', 280, 0.12); break;
      case 'dp': osc('square', 200, 0.14); noise(0.06); break;
      case 'teleport': osc('sine', 640, 0.2); break;
      case 'super': osc('sawtooth', 140, 0.35); noise(0.25); break;
      case 'ko': osc('square', 90, 0.35); break;
      case 'win': osc('triangle', 520, 0.25); break;
      case 'cheer': osc('triangle', 260, 0.12); noise(0.04); break;
      case 'guard': osc('square', 300, 0.05); noise(0.04); break;
      case 'guardbreak': noise(0.14); osc('square', 240, 0.18); osc('triangle', 420, 0.08); break;
      case 'ranged_fire': osc('triangle', 380, 0.10); noise(0.06); break;
      case 'ranged_hit': noise(0.10); osc('square', 160, 0.08); break;
    } }catch(e){}
    }
    function voice(text){ try{ if(!voiceEnabled) return; const u = new SpeechSynthesisUtterance(text); u.lang='zh-CN'; u.rate=0.95; u.pitch=1.0; speechSynthesis.cancel(); speechSynthesis.speak(u);}catch(e){} }
    return { ensure, sfx, voice, toggleVoice(){ voiceEnabled=!voiceEnabled; return voiceEnabled; } };
  })();

  // 输入
  const keys = new Set();
  const keyDownAt = new Map();
  window.spriteSmooth = true; window.spriteShadow = true; window.forceVector = false;
  window.addEventListener('keydown',e=>{ if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault(); if(['1','2','3','4'].includes(e.key)) e.preventDefault(); AudioEngine.ensure(); keys.add(e.key); keyDownAt.set(e.key, performance.now()); if(e.key.toLowerCase()==='r') resetMatch(); if(e.key.toLowerCase()==='v'){ const on=AudioEngine.toggleVoice(); toast(on? '语音：开':'语音：关'); } if(e.key.toLowerCase()==='t'){ cycleStageTheme(); } if(e.key.toLowerCase()==='y'){ window.spriteSmooth=!window.spriteSmooth; window.spriteShadow = window.spriteSmooth; toast(window.spriteSmooth? '角色风格：高清' : '角色风格：像素'); } });
  window.addEventListener('keyup',e=>{ keys.delete(e.key); keyDownAt.delete(e.key); });

  // 直接拖拽/粘贴图片快速替换（左侧=草薙京，右侧=大蛇）
  (function setupQuickImport(){
    const el = canvas;
    function prevent(e){ e.preventDefault(); }
    ['dragenter','dragover'].forEach(ev=> el.addEventListener(ev, e=>{ prevent(e); e.dataTransfer.dropEffect='copy'; }));
    el.addEventListener('drop', async e=>{
      prevent(e);
      const file = e.dataTransfer?.files?.[0]; if(!file) return;
      const rect = el.getBoundingClientRect(); const x = e.clientX - rect.left;
      const targetKey = (x < el.width/2) ? 'kyo_idle' : 'orochi_idle';
      try{
        const data = await ImportManager.fromFile(file);
        let cut = await AutoImport.removeBgSmart(data);
         const isKyo = targetKey==='kyo_idle';
         if(isKyo){ try{ cut = await AutoImport.augmentLegsIfNeededKyo(cut); }catch(e){} }
         ImportManager.set(targetKey, cut);
         const c = isKyo? SpriteEngine.cfg.kyo : SpriteEngine.cfg.orochi;
         try{ const gen = await AutoImport.generateSheetFromImage(cut, c.frameW, c.frameH, c.cols, c.rows); ImportManager.set(isKyo? 'kyo_sheet':'orochi_sheet', gen); }catch(e){}
         SpriteEngine.clear(); await resetRound();
         toast(isKyo? '草薙京素材已替换（自动加腿+抠图）' : '大蛇素材已替换（已抠图）');
      }catch(err){ toast('导入失败，请使用导入面板或提供图片直链'); }
    });
    document.addEventListener('paste', async e=>{
      const items = e.clipboardData?.items || []; let file=null;
      for(const it of items){ if(it.kind==='file'){ file = it.getAsFile(); break; } }
      if(!file) return;
      try{
        // 默认先替换缺失的一方；若都已存在则替换草薙京
        const targetKey = ImportManager.get('kyo_idle')? (!ImportManager.get('orochi_idle')? 'orochi_idle':'kyo_idle') : 'kyo_idle';
        const data = await ImportManager.fromFile(file);
         let cut = await AutoImport.removeBgSmart(data);
         const isKyo = targetKey==='kyo_idle';
         if(isKyo){ try{ cut = await AutoImport.augmentLegsIfNeededKyo(cut); }catch(e){} }
         ImportManager.set(targetKey, cut);
         const c = isKyo? SpriteEngine.cfg.kyo : SpriteEngine.cfg.orochi;
         try{ const gen = await AutoImport.generateSheetFromImage(cut, c.frameW, c.frameH, c.cols, c.rows); ImportManager.set(isKyo? 'kyo_sheet':'orochi_sheet', gen); }catch(e){}
         SpriteEngine.clear(); await resetRound();
         toast(isKyo? '草薙京素材已替换（粘贴+加腿+抠图）' : '大蛇素材已替换（粘贴+抠图）');
      }catch(err){ toast('粘贴图片失败，请改用拖拽或导入面板'); }
    });
  })();

  // 方向缓存用于必杀（qcf/dp/qcb）
  function DirBuffer(){
    const buf=[]; const ttl=500; // ms
    return {
      push(x){ buf.push({x,t:now()}); while(buf.length && now()-buf[0].t>ttl) buf.shift(); },
      hasSeq(seq){ let i=0; for(const s of seq){ let found=false; for(;i<buf.length;i++){ if(buf[i].x===s){ found=true; i++; break; } } if(!found) return false; } return true; },
      clear(){ buf.length=0; }
    };
  }

  // 玩家对象 + 动画控制
  function Player(id, side){
    const skinIdx = 0;
    const skinPack = id===1? SKINS.kyo : SKINS.orochi;
    const base = skinPack[skinIdx];
    return {
      id,
      name: id===1? '草薙京':'大蛇',
      skinIndex: skinIdx,
      color1: base.c1,
      color2: base.c2,
      x: side==='L'? W*0.25: W*0.75,
      y: GROUND,
      w: 70, h: 150,
      vx:0, vy:0, face: side==='L'?1:-1,
      state:'idle', onGround:true, canAct:true,
      hpMax: 1000, hp: 1000,
      powMax: 400, pow: 0,
      wins:0,
      dirBuf: DirBuffer(),
      lockUntil:0,
      frame:0,
      anim:{ key:'idle', idx:0, t:0 },
      invulUntil:0,
      guardType:'stand',
      maxModeUntil:0,
      comboCount:0,
      comboTimer:0,
      actionStartedAt:0,
      actionDur:0,
      guardMax:300,
        guard:300,
        guardBrokenUntil:0,
        ukemiWindowUntil:0,
        lastNormalType:'',
        normalChainCount:0,
        hitConfirmedUntil:0,
        rangedNextAt:0,
        blinkNextAt:0,
        airJumpsLeft:0,
        lastUpJumpAt:0,
        didDoubleJump:false,
        djLightCooldownUntil:0,
        slamOnLanding:false,
        slowUntil:0,
        poisonUntil:0,
        poisonTickNextAt:0,
        vanishUntil:0,
        orochiSlamOnLanding:false
      };
  }
  function applySkin(pl, delta=1){ const pack = pl.id===1? SKINS.kyo : SKINS.orochi; pl.skinIndex = (pl.skinIndex + delta + pack.length) % pack.length; pl.color1 = pack[pl.skinIndex].c1; pl.color2 = pack[pl.skinIndex].c2; }

  // 控制映射
  const P1 = { left:'a', right:'d', up:'w', down:'s', A:'j', B:'k', C:'u', D:'i', BLINK:'l', SKIN:'h' };
  const P2 = { left:'ArrowLeft', right:'ArrowRight', up:'ArrowUp', down:'ArrowDown', A:'1', B:'2', C:'3', D:'4', SUPER:'5', BLINK:'3', SKIN:'9' };

  // 赛局
  let p1, p2, round=1, roundTime=60, running=true, paused=false, msg='';
  let crowdBoostUntil=0, themeBannerUntil=0, themeBannerText=''; let stageAmbientNextAt=0;
  function triggerCrowdBoost(ms){ crowdBoostUntil = now()+ms; }
  function showThemeBanner(text, ms=1200){ themeBannerText=text; themeBannerUntil = now()+ms; }

  async function resetRound(){
    p1 = Player(1,'L'); p2 = Player(2,'R');
    roundTime = 120; msg=''; running=true; paused=false;
    AudioEngine.voice(`第${round}回合，开始！`);
    AudioEngine.sfx('cheer');
    spawnConfetti(W*0.5, H*0.34, 120);
    triggerCrowdBoost(1200);
    await Promise.all([SpriteEngine.ensure('kyo'), SpriteEngine.ensure('orochi')]).catch(()=>{});
  }
  function resetMatch(){ round=1; p1 && (p1.wins=0); p2 && (p2.wins=0); resetRound(); }

  // 物理 & 交互
  const GRAV= 1800, WALK= 280, JUMP= 900;
  const CANCEL = { normal:{start:80, end:260}, specialToSuper:{start:180, end:520} };
  // 场景主题配置（草地/沙地/土场）
  const STAGE_THEME = {
    soil: { groundTop:'#1b202a', groundBottom:'#151821', textureAlpha:0.05, highlightAlpha:0.03, fence:'#6f6b6b' },
    grass:{ groundTop:'#1e3a2a', groundBottom:'#15261b', textureAlpha:0.06, highlightAlpha:0.04, fence:'#6b8f52' },
    sand: { groundTop:'#3a2f24', groundBottom:'#2a241d', textureAlpha:0.06, highlightAlpha:0.05, fence:'#a17c49' }
  };
  const STAGE_ORDER = ['soil','grass','sand']; let stageThemeIdx=0;
  function cycleStageTheme(){ stageThemeIdx=(stageThemeIdx+1)%STAGE_ORDER.length; const name=STAGE_ORDER[stageThemeIdx]; const label = name==='soil'?'土场':name==='grass'?'草地':'沙地'; toast(`场景主题：${label}`); showThemeBanner(`场景：${label}`, 1200); }
   const CHAIN = {
     stand: {
       AA: { window:{start:60,end:160}, lock:400, dmg:35,  blowX:140, blowY:-90,  flags:{} },
       AB: { window:{start:60,end:160}, lock:800, dmg:60, blowX:170, blowY:-110, flags:{} }
     },
     crouch: {
       AA: { window:{start:70,end:170}, lock:400, dmg:35,  blowX:130, blowY:-90,  flags:{low:true} },
       AB: { window:{start:70,end:170}, lock:800, dmg:60, blowX:150, blowY:-110, flags:{low:true} }
     },
     air: {
       AB: { window:{start:50,end:140}, lock:800, dmg:60, blowX:160, blowY:-80,  flags:{overhead:true} }
     }
   };
  const LANDING = { airA:{hit:120, whiff:160}, airB:{hit:100, whiff:180} };
  const effects=[]; // 视觉特效
  function spawnImpact(x,y,color){ effects.push({type:'impact', x, y, t:0, life:220, color}); }
  function spawnDust(x,y){ const n=10; for(let i=0;i<n;i++){ const vx = (Math.random()*2-1)*60; const vy = -Math.random()*100; effects.push({type:'dust', x, y, vx, vy, t:0, life:260, color:'rgba(255,255,255,0.6)'}); } }
  function spawnConfetti(x,y,count=60){
    const palette = ['#ffd56b','#70f0ff','#f0642a','#9bf0ff','#ff6b6b'];
    for(let i=0;i<count;i++){
      const vx = (Math.random()*2-1)*220;
      const vy = -Math.random()*320 - 60;
      const size = 3 + Math.random()*3;
      const col = palette[i%palette.length];
      effects.push({type:'confetti', x, y, vx, vy, t:0, life:1200, size, color:col, spin:(Math.random()*2-1)*2});
    }
  }
  function tickStageAmbient(){
    if(now()<stageAmbientNextAt) return;
    const themeName = STAGE_ORDER[stageThemeIdx];
    if(themeName==='grass'){
      const n = 2 + Math.floor(Math.random()*2);
      for(let i=0;i<n;i++){
        const x = Math.random()*W;
        const y = H*0.40 + Math.random()*20;
        const vx = (Math.random()*2-1)*30;
        const vy = 20 + Math.random()*30;
        const size = 5 + Math.random()*3;
        effects.push({type:'leaf', x, y, vx, vy, t:0, life:2000, size, color:'#6b8f52', spin:Math.random()*Math.PI});
      }
      stageAmbientNextAt = now()+380;
    } else if(themeName==='sand'){
      const n = 3 + Math.floor(Math.random()*2);
      for(let i=0;i<n;i++){
        const x = Math.random()*W;
        const y = GROUND + 4 + Math.random()*10;
        const vx = (Math.random()*2-1)*20;
        const vy = -40 - Math.random()*30;
        effects.push({type:'sand', x, y, vx, vy, t:0, life:1200, color:'rgba(161,124,73,0.6)'});
      }
      stageAmbientNextAt = now()+280;
    } else {
      stageAmbientNextAt = now()+520;
    }
  }
  let shakeUntil=0, shakeMag=0;
   function triggerShake(ms, mag){ shakeUntil = now()+ms; shakeMag = mag; }
   function inputFor(pl, map){
     if(paused || !running) return;
    const l = keys.has(map.left), r = keys.has(map.right), u = keys.has(map.up), d = keys.has(map.down);
    const A = keys.has(map.A), B = keys.has(map.B), Ck = keys.has(map.C), Dk = keys.has(map.D);
    const AB = A && B; const CD = Ck && Dk;
    const dirX = (r?1:0) + (l?-1:0);
    const foe = pl===p1? p2 : p1;

    // 皮肤切换
    if(keys.has(map.SKIN)){ keys.delete(map.SKIN); applySkin(pl, +1); toast(`${pl.id===1?'P1':'P2'} 皮肤：#${pl.skinIndex+1}`); }

    // 方向缓存（前为面向方向）
    if(dirX!==0){ pl.dirBuf.push(dirX===pl.face? 'F':'B'); } else if(d){ pl.dirBuf.push('D'); }
    // 面向：取消自动转向，仅在发生水平移动时根据移动方向更新
    if(dirX!==0){ pl.face = dirX; }

    // MAX模式启动：同时按C+D且气满
    if(CD && pl.canAct && pl.onGround && pl.pow>=pl.powMax){ pl.pow-=pl.powMax; pl.maxModeUntil = now()+8000; toast(`${pl.name} MAX模式！`); spawnImpact(pl.x, pl.y-120, '#ffd56b'); }

    // 防御键：按下“down”（P1=s，P2=ArrowDown）进入防御，减伤80%
    const guardHeld = d;
    if(pl.onGround && guardHeld && now()>=pl.guardBrokenUntil){ pl.state='guard'; pl.guardType='stand'; pl.vx=0; }
    else if(pl.state==='guard'){
      if(guardHeld && pl.onGround) pl.state='guard';
      else if(dirX!==0 && pl.onGround) pl.state='walk';
      else if(pl.onGround) pl.state='idle';
    }

    // Guard Cancel：防御中按A+B翻滚 或 C+D吹飞（消耗气）
    if(pl.state==='guard' && pl.onGround){
      if(AB && pl.pow>=200){ pl.pow-=200; pl.state='roll'; pl.invulUntil = now()+450; pl.vx = pl.face*220; pl.lockUntil = now()+450; keys.delete(map.A); keys.delete(map.B); }
      else if(CD && pl.pow>=300){ pl.pow-=300; pl.state='attack'; pl.lockUntil = now()+320; const inRange = Math.abs(pl.x - foe.x) < (pl.w/2 + foe.w/2 + 60) && Math.abs(pl.y-foe.y) < 100; if(inRange) { hit(foe, 120, 280*pl.face, -60, 'blowback', {high:true}); } keys.delete(map.C); keys.delete(map.D); }
    }

    // 跑步（双击前）
    const hasFF = pl.dirBuf.hasSeq(['F','F']);
    const RUN = 420;
    if(pl.canAct && pl.onGround && hasFF){ pl.state='run'; pl.vx = pl.face*RUN; if(now()<pl.slowUntil) pl.vx *= 0.3; }
    const forwardHeld = (pl.face===1? r : l);
    if(pl.state==='run' && !forwardHeld){ pl.state='idle'; }

    // 短跳（快速轻点上） + 二连跳
    function pressedWithin(key, ms){ const t = keyDownAt.get(key)||0; return (performance.now() - t) <= ms; }
    // 地面起跳：设置一次空中跳次数
    if(pl.canAct && u && pl.onGround){
      const shortHop = pressedWithin(map.up, 120);
      pl.vy = -(shortHop? JUMP*0.7 : JUMP);
      pl.onGround=false; pl.state='jump';
      pl.airJumpsLeft = 1; // 允许一次二连跳
      pl.lastUpJumpAt = keyDownAt.get(map.up)||now();
      AudioEngine.sfx('jump');
    }
    // 二连跳（空中再次按上，需与上次起跳有间隔）
    if(pl.canAct && u && !pl.onGround && pl.airJumpsLeft>0){
      const lastPress = keyDownAt.get(map.up)||0;
      if(lastPress > (pl.lastUpJumpAt||0) + 80){
        const shortHop = pressedWithin(map.up, 120);
        const power = shortHop? JUMP*0.7 : JUMP*0.9;
        pl.vy = -power;
        pl.state='jump';
        pl.airJumpsLeft--;
        pl.lastUpJumpAt = lastPress;
        pl.didDoubleJump = true; // 标记已触发二连跳（用于空中轻击）
        AudioEngine.sfx('jump');
      }
    }

    // 小奥义：草薙京按 I，蓄力2秒消耗200能量，投掷2格火球
    if(pl.id===1 && pl.canAct && pl.onGround && Dk && pl.pow>=200){
       pl.state='super'; locked(2000);
       pl.vx = 0; // 蓄力期间不前进，避免“带着火球过去”观感
       pl.pow -= 200;
       // 蓄力特效（跟随角色，2秒）
       effects.push({ type:'kyo_super_charge', owner:pl.id, t:0, life:2000 });
       AudioEngine.sfx('heavy');
       toast('小奥义');
       keys.delete(map.D);
       return;
     }

    // 大蛇小奥义：P2按 5，飞向高空蓄力2秒→瞬隐0.5秒→对手上方砸下
    if(pl.id===2 && pl.canAct && pl.onGround && keys.has(map.SUPER) && pl.pow>=200){
      pl.state='super'; locked(2500);
      pl.vx = 0; pl.vy = -900; pl.onGround=false; // 起跳上升
      pl.pow -= 200;
      effects.push({ type:'orochi_super_fly', owner:pl.id, t:0, life:2000, targetY: Math.max(H*0.25, pl.y - 300) });
      AudioEngine.sfx('heavy');
      toast('小奥义');
      keys.delete(map.SUPER);
      return;
    }

    // 翻滚（A+B）
    if(pl.canAct && pl.onGround && AB){ pl.state='roll'; pl.invulUntil = now()+450; pl.vx = pl.face*220; pl.lockUntil = now()+450; keys.delete(map.A); keys.delete(map.B); }

    // 受身：被击中落地后短窗A+B快速起身并后翻
    if(pl.state==='hit' && pl.onGround && AB && now() < pl.ukemiWindowUntil){
      pl.state='roll';
      pl.invulUntil = now()+320;
      pl.vx = -pl.face*240;
      pl.lockUntil = now()+320;
      keys.delete(map.A); keys.delete(map.B);
      toast('受身');
      return;
    }

    // 闪身（P1=L，P2=3）：瞬移到当前移动方向的4格外；10秒冷却；允许空中闪身；落点与对手重叠时微调到对手外侧一格
    if(pl.canAct && keys.has(map.BLINK) && now()>=pl.blinkNextAt){
      const dir = (dirX!==0? dirX : pl.face);
      const isKyo = pl.id===1;
      const grid = isKyo? SpriteEngine.cfg.kyo.frameW : SpriteEngine.cfg.orochi.frameW;
      const dist = grid * 4;
      const fromX = pl.x;
      let toX = clamp(pl.x + dir*dist, pl.w/2+20, W - pl.w/2 - 20);
      const overlap = Math.abs(toX - foe.x) < (pl.w/2 + foe.w/2);
      if(overlap){
        // 推到对手外侧一格
        toX = clamp(foe.x + dir*(foe.w/2 + pl.w/2 + grid), pl.w/2+20, W - pl.w/2 - 20);
      }
      spawnDust(fromX, GROUND+2);
      pl.x = toX;
      spawnDust(pl.x, GROUND+2);
      AudioEngine.sfx('teleport');
      pl.blinkNextAt = now()+10000;
      if(keys.has(map.BLINK)) keys.delete(map.BLINK);
      return;
    }

    // 近身投（前/后 + C或D，需近身）
    const closeRange = Math.abs(pl.x - foe.x) < (pl.w/2 + foe.w/2 + 20) && Math.abs(pl.y-foe.y) < 90;
    if(pl.canAct && pl.onGround && closeRange && (Ck||Dk) && (l||r)){
      pl.state='throw'; pl.lockUntil = now()+380; const dir = (r?1: (l?-1:pl.face)); hitThrow(foe, 160, 260*dir, -120); keys.delete(map.C); keys.delete(map.D); return; }
    // 远程攻击（P1=U触发C键；P2仅4键），1秒冷却；Kyo橙色火焰，Orochi蓝色能量波；可跳跃躲避；最远6格
    if(pl.canAct && pl.onGround && (((pl.id===1 && Ck)) || (pl.id===2 && Dk)) && now()>=pl.rangedNextAt){
      // 近身优先投掷，非近身才发射
      if(!closeRange){
        const attacker = pl;
        const isKyo = attacker.id===1;
        const color = (isKyo? '#ff9b4a' : '#70f0ff');
        const y = attacker.y - (isKyo? 56 : 64); // 飞行高度微调
        const vx = attacker.face * (isKyo? 480 : 620); // 能量波更快
        const life = (isKyo? 1600 : 1800); // 寿命差异
        const vThresh = (isKyo? 60 : 70); // 垂直判定厚度：确保站立可被命中，空中另行降低
        const startX = attacker.x + attacker.face*(attacker.w/2 + 12);
        const grid = isKyo? SpriteEngine.cfg.kyo.frameW : SpriteEngine.cfg.orochi.frameW;
         const maxRange = grid * 6; // 6格最大距离（以角色帧宽为单位）
        effects.push({ type:'projectile', owner:attacker.id, x:startX, y, vx, t:0, life, color, size:(isKyo?12:10), vThresh, startX, maxRange });
        AudioEngine.sfx('ranged_fire');
        // 出手锁定：0.2秒，增强手感
        attacker.lockUntil = now()+200; attacker.canAct=false; attacker.actionStartedAt=now(); attacker.actionDur=200; attacker.state='attack';
        attacker.rangedNextAt = now()+1000;
        if(attacker.id===1){ keys.delete(map.C); } else { keys.delete(map.D); }
        return;
      }
    }
    // 跳跃远程攻击（空中，P1=C；P2=4），单段跳与二连跳均可触发；1秒冷却
    if(pl.canAct && !pl.onGround && (((pl.id===1 && Ck)) || (pl.id===2 && Dk)) && now()>=pl.rangedNextAt){
      const attacker = pl;
      const isKyo = attacker.id===1;
      if(isKyo){
        const baseSpeed = 520;
        const angles = [75,50,25];
        const startX = attacker.x + attacker.face*(attacker.w/2);
        const startY = attacker.y - 80;
        const color = '#ff9b4a';
        for(const deg of angles){ const rad = deg*Math.PI/180; const vx = Math.cos(rad) * baseSpeed * attacker.face; const vy = Math.sin(rad) * baseSpeed; effects.push({ type:'airshot', owner:attacker.id, x:startX, y:startY, vx, vy, t:0, life:1200, color, size:10, vThresh:60 }); }
        AudioEngine.sfx('ranged_fire');
      }else{
        const speed = 560; const rad = 45*Math.PI/180;
        const startX = attacker.x + attacker.face*(attacker.w/2);
        const startY = attacker.y - 90;
        const vx = Math.cos(rad)*speed*attacker.face; const vy = Math.sin(rad)*speed;
        effects.push({ type:'poison_spray', owner:attacker.id, x:startX, y:startY, vx, vy, t:0, life:600, color:'#70f0ff', size:10 });
        AudioEngine.sfx('ranged_fire');
      }
      attacker.lockUntil = now()+200; attacker.canAct=false; attacker.actionStartedAt=now(); attacker.actionDur=200; attacker.state='attack';
      attacker.rangedNextAt = now()+2000; // 跳跃远程攻击冷却2秒
      if(attacker.id===1){ keys.delete(map.C); } else { keys.delete(map.D); }
      return;
    }

    // 移动
    if(pl.canAct && pl.state!=='guard' && pl.state!=='roll' && pl.state!=='throw'){
      pl.vx = dirX * WALK;
      if(now()<pl.slowUntil) pl.vx *= 0.3; // 毒液减速
      if(d && pl.onGround){ pl.state='crouch'; if(dirX===0) pl.vx=0; }
      else if(dirX!==0 && pl.onGround && pl.state!=='run') pl.state='walk';
      else if(pl.onGround && dirX===0 && !d) pl.state='idle';
    }

    // 攻击与招式
    function locked(ms){ pl.lockUntil = now()+ms; pl.canAct=false; pl.actionStartedAt=now(); pl.actionDur=ms; }
    function scaleByCombo(pl, base){ const t = now(); if(t - pl.comboTimer <= 600) pl.comboCount++; else pl.comboCount=1; pl.comboTimer=t; const maxBoost = (pl.maxModeUntil>now()? 1.2:1.0); const scale = Math.pow(0.85, Math.max(0, pl.comboCount-1)); return Math.floor(base * maxBoost * scale); }
    function hit(target, dmg, blowX=0, blowY=0, kind='normal', flags={}){
      if(now() < target.invulUntil) return;
      // 格挡判定：站/蹲 vs 高/低/中/崩
      const isOverhead = !!flags.overhead; const isLow = !!flags.low; const blocking = (target.state==='guard' && target.onGround);
      // 仅正面可防御：命中方向与目标朝向一致（基于攻击者相对位置）
      const attacker = (target===p1? p2 : p1);
      const dirHit = sign(attacker.x - target.x);
      const facingFront = (dirHit === target.face);
      let blocked = false;
      if(blocking && facingFront){ if(isOverhead && target.guardType==='crouch') blocked=false; else if(isLow && target.guardType==='stand') blocked=false; else blocked=true; }
      let realDmg = dmg;
      // Guard Break：仅对正在防御的目标破防；伤害固定40
      if(kind==='guardbreak'){
        realDmg = 40; // 破防技只造成40伤害
        const isGuardState = (target.state==='guard');
        if(isGuardState){
          blocked = false; // 忽略减伤，直接破防
          target.guardBrokenUntil = now()+1600;
          target.guard = 0;
          target.onGround = false;
          toast('破防！');
          AudioEngine.sfx('guardbreak');
          triggerShake(280, 7);
        }
      }
      else if(blocked){
        const guardFactor = 0.2; // 防御状态减少80%伤害
        realDmg = Math.floor(dmg*guardFactor);
        blowX*=0.35; blowY*=0.2; AudioEngine.sfx('guard');
        /* defender pow gain on guard removed: energy only from damage dealt */
        // 守挡槽消耗与破防
        const guardCostBase = (kind==='super'? 0.9 : (kind==='special'? 0.7 : 0.4));
        target.guard = clamp(target.guard - Math.floor(dmg*guardCostBase), 0, target.guardMax);
        if(target.guard<=0){
          target.guardBrokenUntil = now()+1600;
          // 破防时给予少量芯片伤害与更强击退
          realDmg = Math.max(realDmg, Math.floor(dmg*0.15));
          blowX *= 0.6; blowY = -120;
        }
      }
      // 命中与破防：给予正常伤害与更长硬直
      target.hp = clamp(target.hp - realDmg, 0, target.hpMax);
      target.vx += blowX; target.vy += blowY;
      if(blocked && !(target.guardBrokenUntil>now())){ target.onGround=true; target.vy=0; } else { target.onGround=false; }
      target.lockUntil = now() + (blocked? 180 : 220);
      if(target.guardBrokenUntil>now()) target.lockUntil = Math.max(target.lockUntil, now()+900);
      target.canAct=false; target.state='hit';
      // 受身窗口：非阻挡命中给予短暂受身时机
      if(!blocked){ target.ukemiWindowUntil = now()+350; }
      // 命中确认：仅命中允许链段
      pl.hitConfirmedUntil = (!blocked ? now()+300 : 0);
      // 能量获取：每造成一点实际伤害，攻方获得一点能量
      pl.pow = clamp(pl.pow + realDmg, 0, pl.powMax);
      /* defender pow gain removed */
      spawnImpact(target.x, target.y-120, pl.color2);
      // 原版风格特效：命中火花/防御火花 + 白闪 + 伤害飘字
      effects.push({type:(blocked? 'guard_spark':'hit_spark'), x:target.x, y:target.y-120, t:0, life:260});
      effects.push({type:'hit_flash', x:target.x, y:target.y-120, t:0, life:120});
      effects.push({type:'dmg', x:target.x, y:target.y-140, t:0, life:800, val:realDmg, color: (blocked? '#9fb5ff' : '#ffd56b'), blocked});
      if(realDmg>=140 && !blocked){ AudioEngine.sfx('heavy'); triggerShake(220, 6); } else if(!blocked){ AudioEngine.sfx('hit'); }
     }
    function hitThrow(target, dmg, blowX, blowY){ target.hp = clamp(target.hp - dmg, 0, target.hpMax); target.vx += blowX; target.vy += blowY; target.onGround=false; target.lockUntil = now()+420; target.canAct=false; target.state='hit'; AudioEngine.sfx('heavy'); spawnImpact(target.x, target.y-130, '#ffd56b'); }

    const inRange = Math.abs(pl.x - foe.x) < (pl.w/2 + foe.w/2 + 60) && Math.abs(pl.y-foe.y) < 90;

    // 普通攻击（A/B）
    if(pl.canAct && pl.onGround && (A||B)){
      const isCrouch = (pl.state==='crouch');
      pl.state='attack';
      if(isCrouch){
        locked(A? 400 : 800);
        const base = A? 35 : 60;
        const kind = (B && guardHeld) ? 'guardbreak' : 'normal';
        if(inRange){ hit(foe, scaleByCombo(pl, base), 140*pl.face, -100, kind, {low:true}); }
        pl.lastNormalType = A? 'crouchA':'crouchB';
        pl.normalChainCount = A? 1 : 0;
      }else{
        locked(A? 400 : 800);
        const base = A? 35 : 60;
        const kind = (B && guardHeld) ? 'guardbreak' : 'normal';
        if(inRange){ hit(foe, scaleByCombo(pl, base), 160*pl.face, -120, kind); }
        pl.lastNormalType = A? 'standA':'standB';
        pl.normalChainCount = A? 1 : 0;
      }
      pl.hitConfirmedUntil = 0;
      keys.delete(map.A); keys.delete(map.B);
      return;
    }
    // 二连跳跃轻击（A，需已使用二连跳且3秒冷却）
    if(pl.canAct && !pl.onGround && A && pl.didDoubleJump && now()>=pl.djLightCooldownUntil){
      if(pl.id===1){
        // 草薙京：向下砸地，落地时在附近造成80伤害
        pl.state='attack'; locked(700);
        pl.vy = Math.max(pl.vy, 0) + 1100; // 加速下落
        pl.slamOnLanding = true; // 落地时结算伤害
        // 周身火焰气场（随身跟随，持续锁定时间）
        effects.push({ type:'aura_fire', owner:pl.id, t:0, life:700 });
        pl.djLightCooldownUntil = now()+3000;
        AudioEngine.sfx('heavy');
        keys.delete(map.A); return;
      }else{
        // 大蛇：空中闪到对手身后爆发蓝色炸气，造成60伤害
        pl.state='attack'; locked(600);
        const fromX = pl.x, fromY = pl.y; // 闪身起点
        const behindX = foe.x - foe.face * (foe.w/2 + pl.w/2 + 12);
        pl.x = clamp(behindX, pl.w/2+20, W - pl.w/2 - 20);
        pl.y = foe.y - 20; pl.onGround=false;
        // 蓝色虚影：沿闪身路径逐渐消失（1s）
        effects.push({ type:'blink_afterimage', x1:fromX, y1:fromY, x2:pl.x, y2:pl.y, t:0, life:1000, color:'#70f0ff' });
        spawnImpact(pl.x, pl.y-100, '#70f0ff');
        const dir = Math.sign(foe.x - pl.x) || 1; // 从玩家指向对手的方向
        if(Math.abs(pl.x - foe.x) < (pl.w/2 + foe.w/2 + 80) && Math.abs(pl.y - foe.y) < 110){
          hit(foe, scaleByCombo(pl, 60), 220*dir, -140, 'normal', {high:true});
          pl.hitConfirmedUntil = now()+120;
        }
        pl.djLightCooldownUntil = now()+3000;
        AudioEngine.sfx('heavy');
        keys.delete(map.A); return;
      }
    }
    // 空中普通攻击（A/B）
    if(pl.canAct && !pl.onGround && (A||B)){
      pl.state='attack'; locked(A? 400 : 800);
      const base = A? 35 : 60;
      if(inRange){ hit(foe, scaleByCombo(pl, base), 150*pl.face, -80, 'normal', {overhead:true}); }
      pl.lastNormalType = A? 'airA':'airB';
      pl.normalChainCount = A? 1 : 0;
      pl.hitConfirmedUntil = 0;
      keys.delete(map.A); keys.delete(map.B);
      return;
    }

    function canCancel(win){ const t = now() - pl.actionStartedAt; const ext = (pl.maxModeUntil>now()? 80:0); return t>=win.start && t<=win.end+ext; }
    // 链取消：仅命中可链（优先于必杀/超杀取消）
    if(pl.state==='attack'){
      const type = (pl.lastNormalType||'').startsWith('air')? 'air' : (pl.lastNormalType||'').startsWith('crouch')? 'crouch' : (pl.lastNormalType||'').startsWith('stand')? 'stand' : '';
      const conf = CHAIN[type];
      const lastWasA = (pl.lastNormalType||'').endsWith('A');
      // A→A（仅站姿/蹲姿，最多2段；仅从A起手）
      if(conf && conf.AA && lastWasA && A && (pl.normalChainCount||0) < 2 && pl.hitConfirmedUntil>now() && canCancel(conf.AA.window)){
        const spec = conf.AA;
        pl.state='attack'; locked(spec.lock);
        if(inRange){ hit(foe, scaleByCombo(pl, spec.dmg), spec.blowX*pl.face, spec.blowY, 'normal', spec.flags||{}); }
        pl.normalChainCount = (pl.normalChainCount||1) + 1;
        pl.lastNormalType = (type==='stand'? 'standA':'crouchA');
        pl.hitConfirmedUntil = 0;
        keys.delete(map.A);
        return;
      }
      // A→B（站姿/蹲姿/空中；仅从A起手）
      if(conf && conf.AB && lastWasA && B && pl.hitConfirmedUntil>now() && canCancel(conf.AB.window)){
        const spec = conf.AB;
        pl.state='attack'; locked(spec.lock);
        const kind = (B && guardHeld) ? 'guardbreak' : 'normal';
        if(inRange){ hit(foe, scaleByCombo(pl, spec.dmg), spec.blowX*pl.face, spec.blowY, kind, spec.flags||{}); }
        pl.lastNormalType = type==='air'? 'airB' : (type==='stand'? 'standB' : 'crouchB');
        pl.normalChainCount = 0;
        pl.hitConfirmedUntil = 0;
        keys.delete(map.B);
        return;
      }
    }
    // 取消规则：普通技 → 必杀/超杀（在窗口内允许）
    const lastWasB = (pl.lastNormalType||'').endsWith('B');
    const winN = CANCEL.normal;
    const winForB = lastWasB ? {start: Math.max(0, winN.start-10), end: winN.end+30} : winN;
    if(pl.state==='attack' && canCancel(winForB)){
      const hasQcf = pl.dirBuf.hasSeq(['D','F']);
      const hasDp  = pl.dirBuf.hasSeq(['F','D','F']);
      const hasQcb = pl.dirBuf.hasSeq(['D','B']);
      if((hasDp && (A||Ck)) || (hasQcf && (B||Dk)) || (hasQcb && (A||Ck))){
        pl.state='special'; locked(420);
        AudioEngine.sfx(hasDp? 'dp' : (hasQcf? 'qcf':'teleport'));
        if(pl.id===1){ if(hasDp) AudioEngine.voice('鬼烧！'); else if(hasQcf) AudioEngine.voice('七十五式·改！'); }
        if(inRange) hit(foe, scaleByCombo(pl, 120), 200*pl.face, -220, 'special');
        keys.delete(map.A); keys.delete(map.B); keys.delete(map.C); keys.delete(map.D);
        pl.dirBuf.clear();
        return;
      }
      // 普攻直接取消超杀（需气或MAX）
      const canSuper = (pl.pow>=pl.powMax || pl.maxModeUntil>now());
      const superInputKyo = pl.dirBuf.hasSeq(['D','B']) && (A||Ck);
      const superInputOrochi = (A||Ck);
      if(canSuper && ((pl.id===1 && superInputKyo) || (pl.id===2 && superInputOrochi))){
        pl.pow=0; pl.state='super'; locked(900); AudioEngine.sfx('super'); if(pl.id===1) AudioEngine.voice('大蛇薙！'); else AudioEngine.voice('毁灭！');
        spawnConfetti(pl.x, pl.y-140, 80);
        AudioEngine.sfx('cheer'); triggerShake(320, 8);
        if(Math.abs(pl.x-foe.x)< 320 && Math.abs(pl.y-foe.y)<140){ hit(foe, 360, 260*pl.face, -420, 'super'); }
        pl.dirBuf.clear(); keys.delete(map.A); keys.delete(map.C); return;
      }
    }
    // 特 → 超取消窗口（MAX或满气更易连）
    if(pl.state==='special' && canCancel(CANCEL.specialToSuper)){
      const canSuper = (pl.pow>=pl.powMax || pl.maxModeUntil>now());
      const superInputKyo = pl.dirBuf.hasSeq(['D','B']) && (A||Ck);
      const superInputOrochi = (A||Ck);
      if(canSuper && ((pl.id===1 && superInputKyo) || (pl.id===2 && superInputOrochi))){
        pl.pow=0; pl.state='super'; locked(900); AudioEngine.sfx('super'); if(pl.id===1) AudioEngine.voice('大蛇薙！'); else AudioEngine.voice('毁灭！');
        spawnConfetti(pl.x, pl.y-140, 80);
        AudioEngine.sfx('cheer'); triggerShake(320, 8);
        if(Math.abs(pl.x-foe.x)< 320 && Math.abs(pl.y-foe.y)<140){ hit(foe, 360, 260*pl.face, -420, 'super'); }
        pl.dirBuf.clear(); keys.delete(map.A); keys.delete(map.C); return;
      }
    }
    // 清理方向缓存
    if(A||B||Ck||Dk) pl.dirBuf.clear();
  }

  function physics(pl, dt){
    if(now()<pl.lockUntil) pl.canAct=false; else pl.canAct=true;
    const wasAirborne = !pl.onGround;
    pl.vy += GRAV*dt;
    pl.x += pl.vx*dt; pl.y += pl.vy*dt;
    if(pl.y>=GROUND){
      pl.y=GROUND; pl.vy=0; pl.onGround=true;
      pl.airJumpsLeft = 0; // 落地重置空中跳次数
      pl.didDoubleJump = false; // 清除本次空中二连跳标记
      // Kyo二连跳轻击：落地爆炎结算
      if(pl.slamOnLanding){
        const foe = (pl===p1? p2 : p1);
        spawnImpact(pl.x, pl.y-120, '#ff9b4a');
        const grid = SpriteEngine.cfg.kyo.frameW;
        const horizRange = (pl.w/2 + foe.w/2 + 120 + grid); // 左右各 +1格
        const inRange = Math.abs(pl.x - foe.x) < horizRange && Math.abs(pl.y - foe.y) < 140;
        if(inRange){
          const dir = Math.sign(foe.x - pl.x) || 1;
          hitG(pl, foe, scaleByComboG(pl, 80), 260*dir, -320, 'normal', {overhead:true});
          pl.hitConfirmedUntil = now()+160;
        }
        pl.slamOnLanding=false;
      }
      // 大蛇小奥义砸落：落地结算100伤害 + 紫色范围震荡
      if(pl.orochiSlamOnLanding && pl.onGround){
        const foe = (pl===p1? p2 : p1);
        spawnImpact(pl.x, pl.y-120, '#b450ff');
        effects.push({type:'ripple', x:pl.x, y:pl.y-100, t:0, life:420, color:'#b450ff'});
        const grid = SpriteEngine.cfg.orochi.frameW;
        const horizRange = (pl.w/2 + foe.w/2 + 100 + grid*1.2); // 左右各 ~1.2格
        const inRange = Math.abs(pl.x - foe.x) < horizRange && Math.abs(pl.y - foe.y) < 160;
        if(inRange){
          const dir = Math.sign(foe.x - pl.x) || 1;
          hitG(pl, foe, 100, 280*dir, -480, 'special', {overhead:true, nopow:true});
          AudioEngine.sfx('heavy');
          pl.hitConfirmedUntil = now()+160;
        }
        pl.orochiSlamOnLanding=false;
      }
      // 空中普通攻击落地硬直（命中更短，空挥更长）
      if(wasAirborne && pl.state==='attack' && (pl.lastNormalType||'').startsWith('air')){
        const t = now();
        const ln = (pl.lastNormalType||'airA');
        const conf = (LANDING[ln] || LANDING.airA);
        const recovery = (pl.hitConfirmedUntil>t ? conf.hit : conf.whiff);
        pl.lockUntil = Math.max(pl.lockUntil, t + recovery);
        pl.canAct = false;
        spawnDust(pl.x, GROUND+2);
        AudioEngine.sfx('land');
      }
      if(pl.state==='jump') pl.state='idle';
    }
    pl.x = clamp(pl.x, pl.w/2+20, W - pl.w/2 - 20);
    // 防御值缓慢恢复（非防御中更快）
    const regen = (pl.state==='guard'? 10 : 30); pl.guard = clamp(pl.guard + regen*dt, 0, pl.guardMax);
    // 中毒DOT：每秒10伤害，持续到 poisonUntil
    if(now()<pl.poisonUntil && now()>=(pl.poisonTickNextAt||0)){
      pl.hp = clamp(pl.hp - 10, 0, pl.hpMax);
      pl.poisonTickNextAt = now()+1000;
    }
    // 简单帧推进（用于回退渲染），详细动画由 updateAnim 处理
    if(pl.state==='walk' || pl.state==='attack' || pl.state==='special' || pl.state==='super' || pl.state==='hit') pl.frame=(pl.frame+1)%32; else pl.frame=0;
  }

  function resolvePush(){
    const dx = (p2.x - p1.x); const minDist = (p1.w/2 + p2.w/2);
    const vertNear = Math.abs(p1.y - p2.y) < 90;
    const bothGround = p1.onGround && p2.onGround;
    if(bothGround && vertNear && Math.abs(dx)<minDist){
      const push = (minDist - Math.abs(dx))/2; const dir = sign(dx);
      p1.x -= push*dir; p2.x += push*dir;
    }
  }

  // 全局伤害与缩放（供特效/弹道调用）
  function scaleByComboG(attacker, base){
    const t = now();
    if(t - attacker.comboTimer <= 600) attacker.comboCount++; else attacker.comboCount=1;
    attacker.comboTimer=t;
    const maxBoost = (attacker.maxModeUntil>now()? 1.2:1.0);
    const scale = Math.pow(0.85, Math.max(0, attacker.comboCount-1));
    return Math.floor(base * maxBoost * scale);
  }
  function hitG(attacker, target, dmg, blowX=0, blowY=0, kind='normal', flags={}){
    if(now() < target.invulUntil) return;
    const isOverhead = !!flags.overhead; const isLow = !!flags.low; const blocking = (target.state==='guard' && target.onGround);
    // 仅正面可防御：命中方向与目标朝向一致（弹道/特效：基于来源位置或攻击者位置）
    const srcX = (flags.sourceX!==undefined? flags.sourceX : attacker.x);
    const dirHit = sign(srcX - target.x);
    const facingFront = (dirHit === target.face);
    let blocked = false; if(blocking && facingFront){ if(isOverhead && target.guardType==='crouch') blocked=false; else if(isLow && target.guardType==='stand') blocked=false; else blocked=true; }
    let realDmg = dmg;
    if(kind==='guardbreak'){
      realDmg = 40; // 破防技只造成40伤害
      const isGuardState = (target.state==='guard');
      if(isGuardState){
        blocked = false;
        target.guardBrokenUntil = now()+1600; target.guard = 0; target.onGround = false;
        toast('破防！'); AudioEngine.sfx('guardbreak'); triggerShake(280, 7);
      }
    } else if(blocked){
      const guardFactor = 0.2; realDmg = Math.floor(dmg*guardFactor);
      blowX*=0.35; blowY*=0.2; AudioEngine.sfx('guard');
      /* defender pow gain on guard removed: energy only from damage dealt */
      const guardCostBase = (kind==='super'? 0.9 : (kind==='special'? 0.7 : 0.4));
      target.guard = clamp(target.guard - Math.floor(dmg*guardCostBase), 0, target.guardMax);
      if(target.guard<=0){ target.guardBrokenUntil = now()+1600; realDmg = Math.max(realDmg, Math.floor(dmg*0.15)); blowX *= 0.6; blowY = -120; }
    }
    target.hp = clamp(target.hp - realDmg, 0, target.hpMax);
    target.vx += blowX; target.vy += blowY;
    if(blocked && !(target.guardBrokenUntil>now())){ target.onGround=true; target.vy=0; } else { target.onGround=false; }
    target.lockUntil = now() + (blocked? 180 : 220);
    if(target.guardBrokenUntil>now()) target.lockUntil = Math.max(target.lockUntil, now()+900);
    target.canAct=false; target.state='hit';
    if(!blocked){ target.ukemiWindowUntil = now()+350; }
    attacker.hitConfirmedUntil = (!blocked ? now()+300 : 0);
    // 能量获取：每造成一点实际伤害，攻方获得一点能量（特殊技不回气）
    if(!(flags && flags.nopow)) attacker.pow = clamp(attacker.pow + realDmg, 0, attacker.powMax);
    /* defender pow gain removed */
    spawnImpact(target.x, target.y-120, attacker.color2);
    // 原版风格特效：命中火花/防御火花 + 白闪 + 伤害飘字
    effects.push({type:(blocked? 'guard_spark':'hit_spark'), x:target.x, y:target.y-120, t:0, life:260});
    effects.push({type:'hit_flash', x:target.x, y:target.y-120, t:0, life:120});
    effects.push({type:'dmg', x:target.x, y:target.y-140, t:0, life:800, val:realDmg, color: (blocked? '#9fb5ff' : '#ffd56b'), blocked});
    if(realDmg>=140 && !blocked){ AudioEngine.sfx('heavy'); triggerShake(220, 6); } else if(!blocked){ AudioEngine.sfx('hit'); }
  }

  // 动画系统（基于atlas）
  function updateAnim(pl, dt){
    const name = pl.id===1? 'kyo':'orochi';
    const conf = SpriteEngine.cfg[name];
    const atlas = conf.atlas;
    const key = atlas[pl.state] ? pl.state : 'idle';
    if(pl.anim.key!==key){ pl.anim.key=key; pl.anim.idx=0; pl.anim.t=0; }
    const baseRate = (atlas[key].rate||120);
    const boost = (pl.maxModeUntil>now()? 0.85 : 1.0);
    const rate = (pl.state==='run'? Math.max(60, baseRate*0.7) : baseRate) * boost;
    pl.anim.t += dt*1000;
    if(pl.anim.t>=rate){ pl.anim.t-=rate; pl.anim.idx = (pl.anim.idx+1) % atlas[key].frames.length; }
  }

  // 回合与胜负
  function tickRound(dt){
    if(paused || !running) return;
    roundTime -= dt; if(roundTime<=0){ const w = p1.hp===p2.hp ? 0 : (p1.hp>p2.hp?1:2); endRound(w); }
    if(p1.hp<=0) endRound(2);
    if(p2.hp<=0) endRound(1);
  }
  function endRound(winner){
    running=false; if(winner===1){ p1.wins++; msg='P1 获胜'; AudioEngine.sfx('win'); } else if(winner===2){ p2.wins++; msg='P2 获胜'; AudioEngine.sfx('win'); } else { msg='平局'; }
    AudioEngine.sfx('ko'); AudioEngine.voice('KO!');
    AudioEngine.sfx('cheer'); spawnConfetti(W*0.5, H*0.34, 240); triggerShake(280,7); triggerCrowdBoost(1500);
    if(p1.wins>=2 || p2.wins>=2){ msg += ' · 比赛结束'; }
    else { setTimeout(()=>{ round++; resetRound(); }, 1400); }
  }

  // 绘制
  function drawStage(){
    // 天空背景（非像素风柔和渐变）
    const isSunset = (round%2===0);
     const sky = ctx.createLinearGradient(0,0,0,H);
     if(isSunset){
       sky.addColorStop(0, '#1a1923');
       sky.addColorStop(0.5, '#3a2f3a');
       sky.addColorStop(1, '#1a141f');
     }else{
       sky.addColorStop(0, C.bgTop);
       sky.addColorStop(0.6, '#101622');
       sky.addColorStop(1, C.bgBottom);
     }
     ctx.fillStyle = sky; ctx.fillRect(0,0,W,H);

    // 远景丘陵（柔和曲线，轻微视差）
    const hillOffset = Math.sin(now()*0.0008)*12;
    ctx.save(); ctx.translate(hillOffset,0); ctx.fillStyle='rgba(80,100,140,0.22)';
    ctx.beginPath(); ctx.moveTo(0, H*0.42);
    ctx.quadraticCurveTo(W*0.28, H*0.36, W*0.5, H*0.42);
    ctx.quadraticCurveTo(W*0.78, H*0.48, W, H*0.44);
    ctx.lineTo(W,0); ctx.lineTo(0,0); ctx.closePath(); ctx.fill();
    ctx.restore();

    // 地面（土地质感：柔和径向加深 + 轻微横向纹理）
    const themeName = STAGE_ORDER[stageThemeIdx]; const theme = STAGE_THEME[themeName];
    const groundGrad = ctx.createLinearGradient(0,GROUND,0,H);
    groundGrad.addColorStop(0, theme.groundTop); groundGrad.addColorStop(1, theme.groundBottom);
    ctx.fillStyle = groundGrad; ctx.fillRect(0,GROUND+2, W, H-GROUND);
    // 地面高光区
    const rGrad = ctx.createRadialGradient(W*0.5, GROUND+40, 40, W*0.5, GROUND+60, W*0.55);
    rGrad.addColorStop(0, 'rgba(255,255,255,'+theme.highlightAlpha+')'); rGrad.addColorStop(1, 'rgba(255,255,255,0)');
    ctx.fillStyle = rGrad; ctx.fillRect(0,GROUND+2, W, H-GROUND);
    // 轻微横向纹理线（非像素风）
    ctx.strokeStyle='rgba(255,255,255,'+theme.textureAlpha+')'; ctx.lineWidth=1;
    ctx.beginPath(); for(let i=0;i<9;i++){ const y=GROUND+16+i*22; ctx.moveTo(18,y); ctx.lineTo(W-18,y);} ctx.stroke();
    // 前景栅栏（主题色）
    ctx.save(); ctx.strokeStyle=theme.fence; ctx.globalAlpha=0.35; ctx.lineWidth=3;
    ctx.beginPath(); ctx.moveTo(20,GROUND+6); ctx.lineTo(W-20,GROUND+6); ctx.stroke();
    ctx.lineWidth=2; for(let x=20;x<=W-20;x+=26){ ctx.beginPath(); ctx.moveTo(x,GROUND+6); ctx.lineTo(x,GROUND-24); ctx.stroke(); }
    ctx.restore();

    // 观众看台（左右两侧），非像素风轮廓 + 摇旗动画
    const t = now();
    function drawSpectator(xBase, yBase, idx, side, layer){
      const boost = (now()<crowdBoostUntil)? 1.6 : 1.0;
      const wave = Math.sin(t*0.006 + idx*0.8) * (layer?1.0:0.6) * boost;
      const bodyH = 48 + (idx%3)*8;
      const bodyW = 22;
      const headR = 8;
      const x = xBase + ((idx%5)-2)*8;
      const y = yBase - bodyH;
      // 身体
      ctx.save(); ctx.translate(x,y);
      ctx.fillStyle = layer? 'rgba(180,190,210,0.30)' : 'rgba(160,170,190,0.20)';
      roundRect(-bodyW/2, 0, bodyW, bodyH, 10, true);
      // 头部
      ctx.beginPath(); ctx.arc(0, -10, headR, 0, Math.PI*2); ctx.fillStyle='rgba(220,230,245,0.6)'; ctx.fill();
      // 手臂（上举）
      ctx.strokeStyle='rgba(220,230,245,0.6)'; ctx.lineWidth=2;
      ctx.beginPath(); ctx.moveTo(0, bodyH*0.4); ctx.lineTo(12, bodyH*0.1 - wave*4); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(0, bodyH*0.4); ctx.lineTo(-12, bodyH*0.1 - wave*4); ctx.stroke();
      // 旗杆与旗帜
      const poleX = side>0? 14 : -14;
      ctx.beginPath(); ctx.moveTo(poleX, bodyH*0.1); ctx.lineTo(poleX, bodyH*-0.25); ctx.stroke();
      const flagX = poleX;
      const sway = Math.sin(t*0.008 + idx) * 6 * (layer?1.2:0.8) * boost;
      ctx.beginPath();
      ctx.moveTo(flagX, bodyH*-0.25);
      ctx.lineTo(flagX + 26 + sway, bodyH*-0.28);
      ctx.lineTo(flagX + 20 + sway*0.6, bodyH*-0.18);
      ctx.closePath();
      const colors = ['#ffd56b','#70f0ff','#f0642a'];
      const baseCol = colors[idx%colors.length];
      const flagGrad = ctx.createLinearGradient(flagX, bodyH*-0.28, flagX + (side>0? 26:26) + sway, bodyH*-0.28);
      flagGrad.addColorStop(0, baseCol);
      flagGrad.addColorStop(1, '#ffffff');
      ctx.fillStyle = flagGrad; ctx.globalAlpha=0.9; ctx.fill();
      ctx.restore();
    }
    // 左右看台分层：后排与前排
    const leftBackY = H*0.455, rightBackY = H*0.455;
    const leftFrontY = H*0.465, rightFrontY = H*0.465;
    for(let i=0;i<14;i++){ const xL = 60 + i*34; drawSpectator(xL, leftBackY, i, -1, 0); }
    for(let i=0;i<14;i++){ const xR = W - (60 + i*34); drawSpectator(xR, rightBackY, i+20, +1, 0); }
    for(let i=0;i<14;i++){ const xL = 60 + i*34; drawSpectator(xL, leftFrontY, i+40, -1, 1); }
    for(let i=0;i<14;i++){ const xR = W - (60 + i*34); drawSpectator(xR, rightFrontY, i+60, +1, 1); }

    // 看台遮挡与阴影条带
    ctx.fillStyle='rgba(0,0,0,0.12)'; ctx.fillRect(0, H*0.46, W, 6);
  }
  function drawChar(pl){
    // 瞬隐阶段：不绘制角色与阴影
    if(now()<pl.vanishUntil){ return; }
    // 动态阴影（非像素风）：随高度缩小
    const alt = Math.max(0, GROUND - pl.y);
    const baseW = 48, baseH = 10;
    const sx = Math.max(24, baseW - alt*0.08);
    const sy = Math.max(4, baseH - alt*0.04);
    ctx.save(); ctx.globalAlpha=0.25; ctx.fillStyle='rgba(0,0,0,0.65)';
    ctx.beginPath(); ctx.ellipse(pl.x, GROUND+2, sx, sy, 0, 0, Math.PI*2); ctx.fill(); ctx.restore();

    const name = pl.id===1? 'kyo':'orochi';
    const spriteDrawn = SpriteEngine.draw(name, pl.x, pl.y, pl.face, pl.state, pl.anim.idx);
    // MAX模式气场（原版风格）：角色周围环状金色柔光
    if(pl.maxModeUntil>now()){
      ctx.save(); ctx.globalAlpha=0.65; ctx.globalCompositeOperation='lighter';
      const r = 36 + Math.sin(now()*0.01)*2;
      const grad = ctx.createRadialGradient(pl.x, pl.y-60, r*0.4, pl.x, pl.y-60, r);
      grad.addColorStop(0,'rgba(255,210,90,0.85)'); grad.addColorStop(1,'rgba(255,210,90,0)');
      ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(pl.x, pl.y-60, r, 0, Math.PI*2); ctx.fill();
      ctx.globalCompositeOperation='source-over'; ctx.restore();
    }
    if(spriteDrawn) return;
    // 回退：人形矢量轮廓（非像素风）
    const x = pl.x, y = pl.y, w=pl.w, h=pl.h;
    ctx.save(); ctx.translate(x, y); ctx.scale(pl.face,1);
    // 颜色与渐变
    const cloth = ctx.createLinearGradient(-w*0.3, -h*0.6, w*0.3, 0);
    cloth.addColorStop(0, pl.color2); cloth.addColorStop(1, pl.color1);
    const skin = ctx.createLinearGradient(0, -h+42, 0, -h+10);
    skin.addColorStop(0, '#e6d3a8'); skin.addColorStop(1, '#d8c195');
    // 躯干（肩部到腰的轻微收窄）
    ctx.fillStyle = cloth; ctx.beginPath();
    ctx.moveTo(-w*0.28, -h*0.62);
    ctx.quadraticCurveTo(-w*0.22, -h*0.70, -w*0.08, -h*0.72); // 左肩
    ctx.lineTo(w*0.08, -h*0.72);
    ctx.quadraticCurveTo(w*0.22, -h*0.70, w*0.28, -h*0.62); // 右肩
    ctx.lineTo(w*0.22, -h*0.22);
    ctx.quadraticCurveTo(0, -h*0.08, -w*0.22, -h*0.22);
    ctx.closePath(); ctx.fill();
    // 头部（圆形）
    ctx.fillStyle = skin; ctx.beginPath(); ctx.arc(0, -h+36, 18, 0, Math.PI*2); ctx.fill();
    // 头发/发带（依据pl.color1高光）
    ctx.fillStyle = pl.color1; ctx.globalAlpha=0.85; ctx.beginPath();
    ctx.ellipse(0, -h+26, 20, 8, 0, 0, Math.PI*2); ctx.fill(); ctx.globalAlpha=1;
    // 手臂（状态驱动摆位）
    const isAttack = (pl.state==='attack'||pl.state==='special'||pl.state==='super');
    const armLen = isAttack? 56 : 44; const foreLen = isAttack? 40 : 28;
    ctx.fillStyle = cloth; ctx.lineWidth=3; ctx.strokeStyle='rgba(0,0,0,0.25)';
    // 手臂：攻击时改为“挥拳”姿态
    if(pl.state==='attack'){
      const dir = pl.face;
      // 前臂（朝面向方向伸出拳）
      const sx = dir>0? w*0.20 : -w*0.20; const sy = -h*0.34;
      ctx.beginPath(); ctx.rect(sx, sy, (dir>0? 46 : -46), 10); ctx.fill(); ctx.stroke();
      // 拳头圆
      ctx.beginPath(); ctx.arc(sx + dir*46, sy+5, 7, 0, Math.PI*2); ctx.fill(); ctx.stroke();
      // 后臂（贴身微弯）
      ctx.beginPath(); ctx.moveTo(-w*0.24, -h*0.48); ctx.quadraticCurveTo(-w*0.28, -h*0.40, -w*0.26, -h*0.32); ctx.lineTo(-w*0.16, -h*0.26); ctx.closePath(); ctx.fill(); ctx.stroke();
    } else {
      // 左臂
      ctx.beginPath(); ctx.moveTo(-w*0.24, -h*0.50); ctx.quadraticCurveTo(-w*0.34, -h*0.42, -w*0.34, -h*0.32); ctx.lineTo(-w*0.34 + armLen*0.28, -h*0.32 + armLen*0.18); ctx.lineTo(-w*0.34 + armLen*0.28 + foreLen*0.36, -h*0.32 + armLen*0.18 + foreLen*0.10); ctx.closePath(); ctx.fill(); ctx.stroke();
      // 右臂
      ctx.beginPath(); ctx.moveTo(w*0.24, -h*0.50); ctx.quadraticCurveTo(w*0.34, -h*0.42, w*0.34, -h*0.32); ctx.lineTo(w*0.34 - armLen*0.24, -h*0.32 + armLen*0.22); ctx.lineTo(w*0.34 - armLen*0.24 - foreLen*0.34, -h*0.32 + armLen*0.22 + foreLen*0.06); ctx.closePath(); ctx.fill(); ctx.stroke();
    }
    // 腿部：走路为“两腿交叉行走”
    ctx.fillStyle = cloth;
    if(pl.state==='walk'){
      const phase = (pl.anim.idx%2);
      if(phase===0){
        // 左腿前跨到右侧，右腿后撤到左侧
        ctx.beginPath(); ctx.moveTo(-w*0.14, -h*0.20); ctx.lineTo(-w*0.06, 0); ctx.lineTo(w*0.02, 0); ctx.lineTo(-w*0.02, -h*0.20); ctx.closePath(); ctx.fill();
        ctx.beginPath(); ctx.moveTo(w*0.14, -h*0.20); ctx.lineTo(w*0.08, 0); ctx.lineTo(-w*0.02, 0); ctx.lineTo(w*0.02, -h*0.20); ctx.closePath(); ctx.fill();
      } else {
        // 右腿前跨到左侧，左腿后撤到右侧
        ctx.beginPath(); ctx.moveTo(w*0.14, -h*0.20); ctx.lineTo(w*0.06, 0); ctx.lineTo(-w*0.02, 0); ctx.lineTo(w*0.02, -h*0.20); ctx.closePath(); ctx.fill();
        ctx.beginPath(); ctx.moveTo(-w*0.14, -h*0.20); ctx.lineTo(-w*0.08, 0); ctx.lineTo(w*0.02, 0); ctx.lineTo(-w*0.02, -h*0.20); ctx.closePath(); ctx.fill();
      }
    } else {
      // 站立/其它：保持原有锥形腿
      ctx.beginPath(); // 左腿
      ctx.moveTo(-w*0.16, -h*0.20); ctx.lineTo(-w*0.10, 0); ctx.lineTo(-w*0.02, 0); ctx.lineTo(-w*0.08, -h*0.20); ctx.closePath(); ctx.fill();
      ctx.beginPath(); // 右腿
      ctx.moveTo(w*0.16, -h*0.20); ctx.lineTo(w*0.10, 0); ctx.lineTo(w*0.02, 0); ctx.lineTo(w*0.08, -h*0.20); ctx.closePath(); ctx.fill();
    }
    // 手套/护腕高光（攻击时增加光晕）
    if(isAttack){
      ctx.globalAlpha=0.8; ctx.fillStyle=pl.color2;
      ctx.shadowColor=pl.color2; ctx.shadowBlur=18;
      ctx.fillRect(w*0.12, -h*0.34, 28, 8);
      ctx.fillRect(-w*0.40, -h*0.34, 28, 8);
      ctx.shadowBlur=0; ctx.globalAlpha=1;
    }
    // 防御描边框（渐变描边 + 轻微发光 + 斜向高光线）
    if(pl.state==='guard'){
      ctx.globalAlpha=0.85;
      const grad = ctx.createLinearGradient(-w*0.35, -h*0.78, w*0.35, -h*0.78);
      grad.addColorStop(0, '#9fb5ff'); grad.addColorStop(1, '#e8f1ff');
      ctx.strokeStyle=grad; ctx.lineWidth=3; ctx.shadowColor='#9fb5ff'; ctx.shadowBlur=12;
      ctx.strokeRect(-w*0.35, -h*0.78, w*0.70, h*0.86);
      // 闪光斜线
      ctx.globalAlpha=0.35; ctx.beginPath(); ctx.moveTo(-w*0.35, -h*0.78+6); ctx.lineTo(w*0.35, -h*0.78+2); ctx.stroke();
      ctx.globalAlpha=1; ctx.shadowBlur=0;
    }
    ctx.restore();
  }
  function roundRect(x,y,w,h,r,fill){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); if(fill) ctx.fill(); }
  function drawHUD(){
    const barW = W*0.36, barH = 20, pad = 16;
    function drawHP(x, hp){ ctx.fillStyle=C.bar; roundRect(x, 10, barW, barH, 10, true); const rate = clamp(hp/1000,0,1); const col = rate>0.6? '#6cf088' : rate>0.3? '#f7c948' : '#ff6b6b'; ctx.fillStyle = col; roundRect(x+2, 12, (barW-4)*rate, barH-4, 8, true); }
    drawHP(pad, p1.hp); drawHP(W - pad - barW, p2.hp);
    // 防御槽（Guard）
    function drawGuard(x, pl){ const w=barW*0.6, h=8; ctx.fillStyle=C.bar; roundRect(x, 34, w, h, 6, true); const rate = clamp(pl.guard/pl.guardMax,0,1); const col = rate>0.6? '#9bf0ff' : rate>0.3? '#f7c948' : '#ff6b6b'; ctx.fillStyle=col; roundRect(x+2, 36, (w-4)*rate, h-4, 4, true); if(now()<pl.guardBrokenUntil){ ctx.strokeStyle='#ff6b6b'; if(Math.floor(performance.now()/120)%2===0) ctx.strokeRect(x, 32, w, h+4); } }
    drawGuard(pad, p1); drawGuard(W - pad - barW*0.6, p2);
    ctx.fillStyle=C.text; ctx.font='700 22px Segoe UI'; ctx.textAlign='center'; ctx.textBaseline='top'; ctx.fillText(String(Math.ceil(roundTime)), W/2, 8);
    function wins(x, n){ for(let i=0;i<2;i++){ ctx.strokeStyle='rgba(255,255,255,0.25)'; ctx.strokeRect(x+i*18, 40,14,14); if(i<n){ ctx.fillStyle='#ffd56b'; ctx.fillRect(x+i*18, 40,14,14);} } }
    wins(pad, p1.wins); wins(W - pad - 36, p2.wins);

    function powBar(x, pl){ ctx.fillStyle=C.bar; roundRect(x, H-32, barW*0.6, 14, 8, true); const rate = clamp(pl.pow/pl.powMax, 0, 1); const maxOn = pl.maxModeUntil>performance.now(); ctx.fillStyle = maxOn ? '#ffd56b' : '#4ea0ff'; roundRect(x+2, H-30, (barW*0.6-4)*rate, 10, 6, true); if(maxOn){ ctx.strokeStyle='#ffd56b'; ctx.strokeRect(x, H-34, barW*0.6, 18); } }
    powBar(pad, p1); powBar(W - pad - barW*0.6, p2);
    if(!running) { ctx.fillStyle=C.text; ctx.font='700 26px Segoe UI'; ctx.fillText(msg, W/2, H*0.18); }
    if(paused){ ctx.fillStyle=C.text; ctx.font='700 20px Segoe UI'; ctx.fillText('暂停 · 空格继续', W/2, H*0.22); }

  }

  // 视觉特效
  function drawEffects(dt){
    for(let i=effects.length-1;i>=0;i--){
      const e=effects[i]; e.t+=dt*1000; const a = clamp(1 - e.t/e.life, 0, 1);
      if(e.type==='impact'){
        // 命中冲击波（保留，作为次级层次）
        ctx.save();
        ctx.globalAlpha=a*0.75; ctx.lineWidth=2;
        for(let r=0;r<2;r++){ ctx.beginPath(); ctx.arc(e.x, e.y, 22+r*10 + e.t*0.03, 0, Math.PI*2); ctx.strokeStyle=e.color; ctx.stroke(); }
        ctx.restore();
      } else if(e.type==='hit_spark'){
        // 原版风格命中火花：十字星 + 碎光，黄橙色
        ctx.save(); ctx.translate(e.x, e.y); ctx.globalAlpha=a; ctx.shadowColor='rgba(255,200,80,0.75)'; ctx.shadowBlur=8;
        const col1 = '#ffd56b', col2 = '#ff9b4a';
        // 十字星
        ctx.strokeStyle=col1; ctx.lineWidth=3; ctx.beginPath(); ctx.moveTo(-16,0); ctx.lineTo(16,0); ctx.moveTo(0,-16); ctx.lineTo(0,16); ctx.stroke();
        // 斜向星
        ctx.strokeStyle=col2; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(-12,-12); ctx.lineTo(12,12); ctx.moveTo(12,-12); ctx.lineTo(-12,12); ctx.stroke();
        // 碎光短线
        for(let k=0;k<6;k++){ const ang = k*Math.PI/3 + Math.sin(e.t*0.02)*0.05; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(Math.cos(ang)*14, Math.sin(ang)*14); ctx.strokeStyle=col2; ctx.lineWidth=2; ctx.stroke(); }
        ctx.shadowBlur=0; ctx.restore();
      } else if(e.type==='guard_spark'){
        // 原版风格防御火花：蓝色十字星 + 外圈薄光
        ctx.save(); ctx.translate(e.x, e.y); ctx.globalAlpha=a; ctx.shadowColor='rgba(130,180,255,0.75)'; ctx.shadowBlur=8;
        const colB = '#9fb5ff';
        ctx.strokeStyle=colB; ctx.lineWidth=3; ctx.beginPath(); ctx.moveTo(-14,0); ctx.lineTo(14,0); ctx.moveTo(0,-14); ctx.lineTo(0,14); ctx.stroke();
        ctx.globalAlpha=a*0.6; ctx.beginPath(); ctx.arc(0,0, 16 + e.t*0.02, 0, Math.PI*2); ctx.strokeStyle='rgba(130,180,255,0.6)'; ctx.lineWidth=2; ctx.stroke();
        ctx.shadowBlur=0; ctx.restore();
      } else if(e.type==='hit_flash'){
        // 受击白闪：命中瞬间短暂白光（更接近原版白闪）
        ctx.save(); ctx.globalAlpha=Math.min(1, a*1.1); ctx.globalCompositeOperation='lighter';
        const grad = ctx.createRadialGradient(e.x, e.y, 6, e.x, e.y, 42);
        grad.addColorStop(0,'rgba(255,255,255,0.9)'); grad.addColorStop(1,'rgba(255,255,255,0)');
        ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(e.x, e.y, 42, 0, Math.PI*2); ctx.fill();
        ctx.globalCompositeOperation='source-over'; ctx.restore();
      } else if(e.type==='dust'){
        // 简单尘土粒子：沿地面散开并稍微上扬
        ctx.save(); ctx.globalAlpha=a*0.8;
        ctx.fillStyle='rgba(200,200,200,0.35)';
        e.x += e.vx*dt; e.y += e.vy*dt; e.vy += 900*dt; // 重力拉回
        if(e.y>GROUND+2) e.y=GROUND+2; // 地面裁切
        for(let k=0;k<2;k++){ ctx.beginPath(); ctx.ellipse(e.x + (k?4:-4), e.y, 8, 3, 0, 0, Math.PI*2); ctx.fill(); }
        ctx.restore();
      } else if(e.type==='confetti'){
        // 彩纸：下落受重力，轻微风摆与旋转，彩色矩形/三角混合
        e.x += e.vx*dt; e.y += e.vy*dt; e.vy += 980*dt;
        e.spin += 0.04;
        ctx.save(); ctx.globalAlpha=a;
        ctx.translate(e.x, e.y);
        ctx.rotate(Math.sin(e.spin)*0.6);
        ctx.fillStyle = e.color;
        if((i+Math.floor(e.t/120))%2===0){ // 矩形片
          ctx.fillRect(-e.size/2, -e.size/2, e.size, e.size*0.6);
        }else{ // 三角片
          ctx.beginPath(); ctx.moveTo(0, -e.size/2); ctx.lineTo(e.size/2, e.size/2); ctx.lineTo(-e.size/2, e.size/2); ctx.closePath(); ctx.fill();
        }
        ctx.restore();
      } else if(e.type==='leaf'){
        e.x += e.vx*dt; e.y += e.vy*dt; e.vy += 220*dt; e.spin += 0.06;
        ctx.save(); ctx.globalAlpha=a*0.9; ctx.translate(e.x, e.y); ctx.rotate(Math.sin(e.spin)*0.6);
        ctx.fillStyle=e.color; ctx.beginPath(); ctx.ellipse(0,0,e.size, e.size*0.5, 0, 0, Math.PI*2); ctx.fill();
        ctx.restore();
      } else if(e.type==='dmg'){
        // 伤害飘字：上浮、淡出、轻微摆动
        e.y -= 60*dt; const wob = Math.sin(e.t*0.02)*2; e.x += wob*dt;
        ctx.save(); ctx.globalAlpha=a; ctx.translate(e.x, e.y);
        ctx.font='700 18px Segoe UI'; ctx.textAlign='center'; ctx.textBaseline='middle';
        ctx.fillStyle=e.color; ctx.strokeStyle='rgba(0,0,0,0.25)'; ctx.lineWidth=3; ctx.strokeText(e.blocked? 'GUARD' : String(e.val), 0, 0);
        ctx.fillText(e.blocked? 'GUARD' : String(e.val), 0, 0);
        ctx.restore();
      } else if(e.type==='aura_fire'){
        // 草薙京：环绕周身的火焰圆环（连续环绕而非花瓣）
        const pl = (e.owner===1? p1 : p2);
        const cx = pl.x, cy = pl.y - 60;
        ctx.save(); ctx.globalAlpha=0.85*a; ctx.globalCompositeOperation='lighter'; ctx.shadowColor='#ff9b4a'; ctx.shadowBlur=14;
        // 外圈火焰环（连续描边）
        const r = 32; ctx.strokeStyle='rgba(255,155,74,0.85)'; ctx.lineWidth=6; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.stroke();
        // 外沿火焰尖（轻微抖动，环绕周身）
        for(let k=0;k<10;k++){
          const ang = k*(Math.PI/5) + Math.sin(e.t*0.01 + k)*0.2;
          const r1 = r, r2 = r + 8;
          ctx.beginPath(); ctx.moveTo(cx + Math.cos(ang)*r1, cy + Math.sin(ang)*r1);
          ctx.lineTo(cx + Math.cos(ang)*r2, cy + Math.sin(ang)*r2);
          ctx.strokeStyle='rgba(255,155,74,0.6)'; ctx.lineWidth=2; ctx.stroke();
        }
        // 内圈柔光
        const inner = ctx.createRadialGradient(cx, cy, 8, cx, cy, 28);
        inner.addColorStop(0,'rgba(255,210,90,0.6)'); inner.addColorStop(1,'rgba(255,210,90,0)');
        ctx.fillStyle=inner; ctx.beginPath(); ctx.arc(cx, cy, 28, 0, Math.PI*2); ctx.fill();
        ctx.globalCompositeOperation='source-over'; ctx.shadowBlur=0; ctx.restore();
      } else if(e.type==='kyo_super_charge'){
        // 小奥义蓄力：角色前方聚能光球
        const pl = (e.owner===1? p1 : p2);
        const cx = pl.x + pl.face*20, cy = pl.y - 60;
        ctx.save(); ctx.globalAlpha=0.9*a; ctx.shadowColor='#ff9b4a'; ctx.shadowBlur=20;
        const rr = 36 + Math.sin(e.t*0.02)*4;
        const g = ctx.createRadialGradient(cx, cy, 10, cx, cy, rr);
        g.addColorStop(0,'rgba(255,210,90,0.7)'); g.addColorStop(1,'rgba(255,155,74,0.0)');
        ctx.fillStyle=g; ctx.beginPath(); ctx.arc(cx, cy, rr, 0, Math.PI*2); ctx.fill();
        ctx.shadowBlur=0; ctx.restore();
        // 完成蓄力：生成大火球（直径2格）
        if(a<=0){
          const grid = SpriteEngine.cfg.kyo.frameW;
          const startX = pl.x + pl.face*(pl.w/2 + 14);
          const startY = pl.y - 62;
          effects.splice(i,1);
          effects.push({ type:'kyo_super_fireball', owner:e.owner, x:startX, y:startY, vx:pl.face*560, t:0, life:1800, color:'#ff9b4a', radius:grid, vThresh:90 });
          AudioEngine.sfx('ranged_fire');
          continue;
        }
      } else if(e.type==='orochi_super_charge'){
        // 大蛇小奥义蓄力：紫色能量涡旋
        const pl = (e.owner===1? p1 : p2);
        const cx = pl.x + pl.face*18, cy = pl.y - 64;
        ctx.save(); ctx.globalAlpha=0.9*a; ctx.shadowColor='rgba(180,80,255,0.9)'; ctx.shadowBlur=16;
        ctx.strokeStyle='rgba(180,80,255,0.8)'; ctx.lineWidth=3;
        for(let r=12; r<=24; r+=6){ ctx.beginPath(); ctx.arc(cx, cy, r + Math.sin(e.t*0.02 + r)*2, 0, Math.PI*2); ctx.stroke(); }
        // 内核柔光
        const g = ctx.createRadialGradient(cx, cy, 4, cx, cy, 26);
        g.addColorStop(0,'rgba(220,120,255,0.7)'); g.addColorStop(1,'rgba(180,80,255,0)');
        ctx.fillStyle=g; ctx.beginPath(); ctx.arc(cx, cy, 26, 0, Math.PI*2); ctx.fill();
        ctx.shadowBlur=0; ctx.restore();
        // 完成蓄力：飞向高空
        if(a<=0){
          effects.splice(i,1);
          effects.push({ type:'orochi_super_fly', owner:e.owner, x:pl.x, y:pl.y, t:0, life:2000, color:'#b450ff', targetY:pl.y-300 });
          AudioEngine.sfx('heavy');
          continue;
        }
      } else if(e.type==='orochi_super_fly'){
        // 飞向高空蓄力：角色持续上升至目标高度，周身紫色涡旋
        const pl = (e.owner===1? p1 : p2);
        const foe = (e.owner===1? p2 : p1);
        pl.state='super'; pl.onGround=false; pl.vx=0;
        // 上升控制：未达到目标高度持续上升，达到后悬停（固定目标高度）
         if(e.targetY === undefined){ e.targetY = Math.max(H*0.25, pl.y - 300); }
         const targetY = e.targetY;
         if(pl.y > targetY){ pl.vy = -1000; } else { pl.vy = 0; pl.y = targetY; }
         // 紫色涡旋视觉
        const cx = pl.x, cy = pl.y - 64;
        ctx.save(); ctx.globalAlpha=0.9*a; ctx.shadowColor='rgba(180,80,255,0.9)'; ctx.shadowBlur=16;
        ctx.strokeStyle='rgba(180,80,255,0.8)'; ctx.lineWidth=3;
        for(let r=12; r<=26; r+=5){ ctx.beginPath(); ctx.arc(cx, cy, r + Math.sin(e.t*0.02 + r)*2, 0, Math.PI*2); ctx.stroke(); }
        const g = ctx.createRadialGradient(cx, cy, 4, cx, cy, 28);
        g.addColorStop(0,'rgba(220,120,255,0.7)'); g.addColorStop(1,'rgba(180,80,255,0)');
        ctx.fillStyle=g; ctx.beginPath(); ctx.arc(cx, cy, 28, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur=0; ctx.restore();
        // 完成蓄力：瞬隐0.5秒并准备砸落
        if(a<=0){
          pl.vanishUntil = now()+300; pl.invulUntil = Math.max(pl.invulUntil, now()+300);
          effects.splice(i,1);
          effects.push({ type:'orochi_super_vanish', owner:e.owner, x:pl.x, y:pl.y, t:0, life:300 });
          continue;
        }
      } else if(e.type==='orochi_super_vanish'){
        // 瞬隐：淡紫色涟漪标记位置，0.5秒后在对手上方出现
        const pl = (e.owner===1? p1 : p2);
        ctx.save(); ctx.globalAlpha=a*0.6; ctx.translate(e.x, e.y);
        ctx.strokeStyle='rgba(180,80,255,0.8)'; ctx.lineWidth=2; for(let r=10;r<=18;r+=4){ ctx.beginPath(); ctx.arc(0,0, r + (e.t*0.02), 0, Math.PI*2); ctx.stroke(); }
        ctx.restore();
        if(a<=0){
          const foe = (e.owner===1? p2 : p1);
          effects.splice(i,1);
          effects.push({ type:'orochi_super_drop', owner:e.owner, x:foe.x, y:foe.y-240, t:0, life:1200 });
          continue;
        }
      } else if(e.type==='orochi_super_drop'){
        // 砸落：在对手上方出现并高速下落，落地后结算伤害
        const pl = (e.owner===1? p1 : p2);
        const foe = (e.owner===1? p2 : p1);
        // 仅在开始瞬间设置位置与下落速度，并交由物理处理
        if(e.t < 60){ pl.vanishUntil = 0; pl.onGround=false; pl.state='attack'; pl.vx=0; pl.x = e.x; pl.y = e.y; pl.vy = 1400; pl.orochiSlamOnLanding = true; }
        // 下落轨迹的紫色拖尾（不覆盖位置）
        ctx.save(); ctx.globalAlpha=a*0.6; ctx.translate(pl.x, pl.y-80);
        ctx.strokeStyle='rgba(180,80,255,0.6)'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(0,-40); ctx.lineTo(0,40); ctx.stroke(); ctx.restore();
        // 结束条件：到时或已落地
        if(a<=0 || pl.onGround){ effects.splice(i,1); }
      } else if(e.type==='blink_afterimage'){
        // 大蛇：蓝色虚影（1s内逐渐消失），沿闪身路径绘制多段残影
        const x1=e.x1, y1=e.y1, x2=e.x2, y2=e.y2; const dx=x2-x1, dy=y2-y1; const len = Math.hypot(dx,dy);
        const n=6; ctx.save(); ctx.globalAlpha=a*0.75; ctx.shadowColor='rgba(112,240,255,0.8)'; ctx.shadowBlur=10;
        for(let i=1;i<=n;i++){
          const t = i/n; const px = x1 + dx*t; const py = y1 + dy*t;
          const fade = (1 - t) * a;
          ctx.globalAlpha = fade;
          ctx.beginPath(); ctx.ellipse(px, py, 12, 6, Math.atan2(dy,dx), 0, Math.PI*2);
          const g = ctx.createRadialGradient(px, py, 2, px, py, 12);
          g.addColorStop(0,'rgba(160,255,255,0.8)'); g.addColorStop(1,'rgba(112,240,255,0.2)');
          ctx.fillStyle=g; ctx.fill();
        }
        // 路径淡线
        ctx.globalAlpha=a*0.35; ctx.strokeStyle='rgba(112,240,255,0.6)'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
        ctx.shadowBlur=0; ctx.restore();
      } else if(e.type==='sand'){
        e.x += e.vx*dt; e.y += e.vy*dt; e.vy += 500*dt; if(e.y>GROUND+2) e.y=GROUND+2;
        ctx.save(); ctx.globalAlpha=a*0.7; ctx.fillStyle=e.color;
        ctx.beginPath(); ctx.ellipse(e.x, e.y, 6, 2.5, 0, 0, Math.PI*2); ctx.fill();
        ctx.restore();
      } else if(e.type==='spark'){
        // 火花：数条放射线与高光三角
        ctx.save(); ctx.globalAlpha=a; ctx.translate(e.x, e.y);
        ctx.strokeStyle=e.color; ctx.lineWidth=2;
        for(let k=0;k<6;k++){ ctx.beginPath(); const ang=k*Math.PI/3; ctx.moveTo(0,0); ctx.lineTo(Math.cos(ang)*16, Math.sin(ang)*16); ctx.stroke(); }
        ctx.globalAlpha=a*0.7; ctx.fillStyle=e.color; ctx.beginPath(); ctx.moveTo(0,-8); ctx.lineTo(10,0); ctx.lineTo(-10,0); ctx.closePath(); ctx.fill();
        ctx.restore();
      } else if(e.type==='ripple'){
        // 波纹：圆形扩散圈（更柔光）
        ctx.save(); ctx.globalAlpha=a*0.95; ctx.translate(e.x, e.y); ctx.shadowColor=e.color; ctx.shadowBlur=12;
        ctx.strokeStyle=e.color; ctx.lineWidth=2; for(let r=10;r<=18;r+=4){ ctx.beginPath(); ctx.arc(0,0,r + (e.t*0.02), 0, Math.PI*2); ctx.stroke(); }
        ctx.shadowBlur=0; ctx.restore();
      } else if(e.type==='projectile'){
        // 直线飞行弹：低空水平飞行，跳起可躲避
        e.x += e.vx*dt;
        const attacker = (e.owner===1? p1 : p2);
        const foe = (e.owner===1? p2 : p1);
        // 绘制：Kyo橙色火焰，Orochi蓝色能量波
        ctx.save(); ctx.globalAlpha=a*0.95; ctx.translate(e.x, e.y);
        ctx.shadowBlur=16;
        if(e.owner===1){ // 橙色火焰：核心 + 尾焰抖动 + 光晕
          ctx.shadowColor=e.color;
          ctx.fillStyle=e.color; ctx.beginPath(); ctx.ellipse(0,0, e.size, e.size*0.7, 0, 0, Math.PI*2); ctx.fill();
          const flick = Math.sin(e.t*0.02)*2;
          ctx.globalAlpha=a*0.55; ctx.fillStyle='rgba(255,155,74,0.5)'; ctx.beginPath(); ctx.ellipse(-12-flick,0, e.size*1.35, e.size*0.55, 0, 0, Math.PI*2); ctx.fill();
        }else{ // 蓝色能量波：双圈 + 光晕
          ctx.shadowColor=e.color;
          ctx.strokeStyle=e.color; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(0,0, e.size-2, 0, Math.PI*2); ctx.stroke();
          ctx.globalAlpha=a*0.6; ctx.strokeStyle='rgba(112,240,255,0.6)'; ctx.beginPath(); ctx.arc(-12,0, e.size+2, 0, Math.PI*2); ctx.stroke();
        }
        // 简易拖尾：在飞行方向后方绘制3个淡化椭圆/圈
        ctx.globalAlpha=a*0.35;
        for(let k=1;k<=3;k++){
          ctx.beginPath();
          ctx.ellipse(-k*6,0, e.size*0.7, e.size*0.45, 0, 0, Math.PI*2);
          if(e.owner===1){ ctx.fillStyle='rgba(255,155,74,0.35)'; ctx.fill(); }
          else { ctx.strokeStyle='rgba(112,240,255,0.35)'; ctx.stroke(); }
        }
        ctx.restore();
        // 碰撞：水平近身且垂直接近；跳起可使垂直差距增大从而躲避
        const dx = Math.abs(e.x - foe.x);
        const dy = Math.abs(e.y - foe.y);
        const hitHoriz = dx < (foe.w/2 + 16);
        const baseV = (e.vThresh||50);
        const hitVert = foe.onGround ? (dy < baseV) : (dy < Math.min(26, baseV*0.45)); // 空中判定更严格，跳起更易躲避
        if(hitHoriz && hitVert){
          const blowX = (e.owner===1? 240 : 210) * (attacker.face);
          const blowY = (e.owner===1? -90 : -80);
          hitG(attacker, foe, scaleByComboG(attacker, 50), blowX, blowY, 'normal', {high:true, sourceX: e.x});
          // 命中特效与音效：火花（Kyo）/ 波纹（Orochi）
          if(e.owner===1){ effects.push({type:'spark', x:foe.x, y:foe.y-100, t:0, life:380, color:'#ff9b4a'}); }
          else { effects.push({type:'ripple', x:foe.x, y:foe.y-100, t:0, life:420, color:'#70f0ff'}); }
          AudioEngine.sfx('ranged_hit');
          effects.splice(i,1);
          continue;
        }
        // 出界或寿命结束或超出最大距离（6格）移除
         if(e.x < -40 || e.x > W+40 || Math.abs(e.x - (e.startX||e.x)) >= (e.maxRange||Infinity)) { effects.splice(i,1); continue; }
      } else if(e.type==='kyo_super_fireball'){
        // 小奥义火球：巨大火球水平飞行，能清除对方投射物与地面毒液
        e.x += e.vx*dt;
        const attacker = (e.owner===1? p1 : p2);
        const foe = (e.owner===1? p2 : p1);
        // 绘制巨大火球
        ctx.save(); ctx.globalAlpha=a*0.95; ctx.translate(e.x, e.y); ctx.shadowColor='#ff9b4a'; ctx.shadowBlur=18;
        ctx.fillStyle=e.color; ctx.beginPath(); ctx.ellipse(0,0, e.radius, e.radius*0.85, 0, 0, Math.PI*2); ctx.fill();
        ctx.globalAlpha=a*0.4; ctx.fillStyle='rgba(255,155,74,0.45)'; ctx.beginPath(); ctx.ellipse(-22,0, e.radius*1.2, e.radius*0.6, 0, 0, Math.PI*2); ctx.fill();
        ctx.shadowBlur=0; ctx.restore();
        // 与对手碰撞
        const dx = Math.abs(e.x - foe.x);
        const dy = Math.abs(e.y - foe.y);
        const hitHoriz = dx < (foe.w/2 + e.radius);
        const baseV = (e.vThresh||80);
        const hitVert = foe.onGround ? (dy < baseV) : (dy < Math.min(30, baseV*0.45));
        if(hitHoriz && hitVert){
          const blowX = 280 * (attacker.face);
          const blowY = -140;
          hitG(attacker, foe, 100, blowX, blowY, 'special', {high:true, sourceX:e.x, nopow:true});
          spawnImpact(foe.x, foe.y-110, '#ff9b4a');
          AudioEngine.sfx('ranged_hit');
          effects.splice(i,1);
          continue;
        }
        // 清除对方投射物与毒液
        for(let j=effects.length-1;j>=0;j--){
          if(j===i) continue;
          const ej = effects[j];
          if(ej.owner===e.owner) continue;
          if(ej.type==='projectile' || ej.type==='airshot' || ej.type==='poison_spray'){
            const dx2 = Math.abs(e.x - ej.x);
            const dy2 = Math.abs(e.y - ej.y);
            const collide = dx2 < (e.radius + (ej.size||14)) && dy2 < (e.radius*0.9 + (ej.vThresh||50));
            if(collide){ effects.splice(j,1); }
          } else if(ej.type==='puddle'){
            const collideP = Math.abs(e.x - ej.x) < (e.radius + ej.width/2);
            if(collideP){ effects.splice(j,1); }
          }
        }
        // 越界或耗尽
        if(e.x < -40 || e.x > W+40 || a<=0){ effects.splice(i,1); continue; }
      } else if(e.type==='airshot'){
        // 空中火球：按给定角度斜下飞行
        e.x += e.vx*dt; e.y += e.vy*dt;
        const attacker = (e.owner===1? p1 : p2);
        const foe = (e.owner===1? p2 : p1);
        ctx.save(); ctx.globalAlpha=a*0.95; ctx.translate(e.x, e.y); ctx.shadowColor=e.color; ctx.shadowBlur=16;
        ctx.rotate(Math.sin(e.t*0.02)*0.15);
        ctx.fillStyle=e.color; ctx.beginPath(); ctx.ellipse(0,0, e.size, e.size*0.8, 0, 0, Math.PI*2); ctx.fill();
        ctx.globalAlpha=a*0.5; ctx.fillStyle='rgba(255,155,74,0.45)'; ctx.beginPath(); ctx.ellipse(-10,0, e.size*1.2, e.size*0.6, 0, 0, Math.PI*2); ctx.fill();
        // 火焰拖尾
        ctx.globalAlpha=a*0.3; for(let k=1;k<=3;k++){ ctx.beginPath(); ctx.ellipse(-k*6,0, e.size*0.6, e.size*0.4, 0, 0, Math.PI*2); ctx.fill(); }
        ctx.shadowBlur=0; ctx.restore();
        const dx = Math.abs(e.x - foe.x);
        const dy = Math.abs(e.y - foe.y);
        const hitHoriz = dx < (foe.w/2 + 14);
        const baseV = (e.vThresh||60);
        const hitVert = foe.onGround ? (dy < baseV) : (dy < Math.min(26, baseV*0.45));
        if(hitHoriz && hitVert){
          const blowX = 180 * (attacker.face);
          const blowY = -120;
          hitG(attacker, foe, scaleByComboG(attacker, 20), blowX, blowY, 'normal', {high:true, sourceX: e.x});
          effects.push({type:'spark', x:foe.x, y:foe.y-100, t:0, life:320, color:'#ff9b4a'});
          AudioEngine.sfx('ranged_hit');
          effects.splice(i,1);
          continue;
        }
        if(e.y>=GROUND-2 || a<=0){ effects.splice(i,1); continue; }
      } else if(e.type==='poison_spray'){
        // 空中毒液喷射：斜下方喷射，触地生成毒液滩
        e.x += e.vx*dt; e.y += e.vy*dt;
        ctx.save(); ctx.globalAlpha=a*0.95; ctx.translate(e.x, e.y); ctx.shadowColor='rgba(112,240,255,0.9)'; ctx.shadowBlur=16;
        const grad = ctx.createRadialGradient(0,0, e.size*0.3, 0,0, e.size);
        grad.addColorStop(0,'rgba(160,255,255,0.8)'); grad.addColorStop(1,'rgba(112,240,255,0.2)');
        ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(0,0, e.size, 0, Math.PI*2); ctx.fill();
        ctx.globalAlpha=a*0.5; ctx.strokeStyle='rgba(112,240,255,0.8)'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(0,0, e.size+2, 0, Math.PI*2); ctx.stroke();
        ctx.shadowBlur=0; ctx.restore();
        if(e.y>=GROUND-2 || a<=0){
          const grid = SpriteEngine.cfg.orochi.frameW;
          effects.push({ type:'puddle', owner:e.owner, x:e.x, y:GROUND, width:grid, t:0, life:2000 });
          effects.splice(i,1);
          continue;
        }
      } else if(e.type==='puddle'){
        // 毒液滩：1格宽，存在2s；踩到者减速并中毒
        const attacker = (e.owner===1? p1 : p2);
        const foe = (e.owner===1? p2 : p1);
        ctx.save();
        // 毒液滩柔和波动的黏液外观 + 边缘高光 + 小气泡
        const w = e.width, half = w/2; const baseY = GROUND-2; ctx.translate(e.x, baseY);
        ctx.globalAlpha=a*0.9; ctx.shadowColor='rgba(112,240,255,0.8)'; ctx.shadowBlur=10;
        ctx.beginPath(); ctx.moveTo(-half, 0);
        const amp = 2 + Math.sin(e.t*0.01)*1.5;
        for(let k=1;k<=4;k++){ const x = -half + k*(w/4); const y = Math.sin(e.t*0.02 + k)*amp; ctx.quadraticCurveTo(x - w/8, y, x, 0); }
        ctx.quadraticCurveTo(half - w/8, Math.sin(e.t*0.02 + 5)*amp, half, 0);
        const grad2 = ctx.createLinearGradient(0,-4,0,4);
        grad2.addColorStop(0,'rgba(112,240,255,0.55)'); grad2.addColorStop(1,'rgba(60,160,170,0.35)');
        ctx.fillStyle=grad2; ctx.fill();
        ctx.globalAlpha=a*0.6; ctx.strokeStyle='rgba(160,255,255,0.5)'; ctx.lineWidth=2; ctx.stroke();
        ctx.globalAlpha=a*0.7;
        for(let b=0;b<3;b++){ const bx = -half + (b+1)*w/4 + Math.sin(e.t*0.01 + b)*3; const by = -2 - Math.sin(e.t*0.015 + b*2); ctx.beginPath(); ctx.arc(bx, by, 3, 0, Math.PI*2); ctx.fillStyle='rgba(160,255,255,0.6)'; ctx.fill(); }
        ctx.shadowBlur=0; ctx.restore();
        const inPuddle = Math.abs(foe.x - e.x) < (foe.w/2 + e.width/2) && foe.onGround;
        if(inPuddle){
          const tnow = now();
          const isNewPoison = (foe.poisonUntil<=tnow);
          foe.slowUntil = Math.max(foe.slowUntil, tnow+3000);
          foe.poisonUntil = Math.max(foe.poisonUntil, tnow+3000);
          if(isNewPoison){
            // 首次踩到后1秒开始掉血，随后每秒掉10点
            foe.poisonTickNextAt = tnow + 1000;
          }
        }
      }
      if(a<=0) effects.splice(i,1);
    }
  }

  // 主循环
  let last = now();
  function loop(){
    const t = now(); const dt = Math.min(0.033, (t-last)/1000); last=t;
    if(keys.has(' ')) paused=!paused, keys.delete(' ');
    if(running && !paused){ inputFor(p1, P1); inputFor(p2, P2); physics(p1, dt); physics(p2, dt); resolvePush(); updateAnim(p1,dt); updateAnim(p2,dt); tickRound(dt); }
    // 屏幕震动包裹
    ctx.save();
    if(now()<shakeUntil){ const sx = (Math.random()*2-1)*shakeMag; const sy = (Math.random()*2-1)*shakeMag; ctx.translate(sx, sy); }
    tickStageAmbient();
    drawStage(); drawChar(p1); drawChar(p2); drawHUD(); drawEffects(dt);
    ctx.restore();
    requestAnimationFrame(loop);
  }

  // Toast 简讯
  let toastTimer=0, toastText='';
  function toast(text){ toastText=text; clearTimeout(toastTimer); toastTimer=setTimeout(()=>toastText='', 1200); }
  (function drawToast(){ requestAnimationFrame(drawToast); })();

  // 导入应用事件（可选）
  if(btnImportApply){
    btnImportApply.addEventListener('click', async ()=>{
      async function setFrom(urlId, fileId, key){
        const url = (document.getElementById(urlId)?.value||'').trim();
        const fileEl = document.getElementById(fileId);
        try{
          if(url){ const data = await ImportManager.fromUrlSmart(url); ImportManager.set(key, data); }
          else if(fileEl && fileEl.files && fileEl.files[0]){ const data = await ImportManager.fromFile(fileEl.files[0]); ImportManager.set(key, data); }
        }catch(e){}
      }
      await setFrom('kyoIdleUrl','kyoIdleFile','kyo_idle');
      await setFrom('kyoSheetUrl','kyoSheetFile','kyo_sheet');
      await setFrom('oroIdleUrl','oroIdleFile','orochi_idle');
      await setFrom('oroSheetUrl','oroSheetFile','orochi_sheet');
      // 若只导入了参考图，则基于参考图生成基础雪碧图
      try{
        if(ImportManager.get('kyo_idle') && !ImportManager.get('kyo_sheet')){
          const c = SpriteEngine.cfg.kyo; const gen = await AutoImport.generateSheetFromImage(ImportManager.get('kyo_idle'), c.frameW, c.frameH, c.cols, c.rows); ImportManager.set('kyo_sheet', gen);
        }
        if(ImportManager.get('orochi_idle') && !ImportManager.get('orochi_sheet')){
          const c = SpriteEngine.cfg.orochi; const gen = await AutoImport.generateSheetFromImage(ImportManager.get('orochi_idle'), c.frameW, c.frameH, c.cols, c.rows); ImportManager.set('orochi_sheet', gen);
        }
      }catch(e){ /* ignore */ }
      SpriteEngine.clear(); await resetRound();
      importModal.style.display='none'; toast('原版皮肤已应用');
    });
  }
  // 清空皮肤缓存（禁用旧像素占位，回退人形渲染）
  const btnImportClear = document.getElementById('btn-import-clear');
  if(btnImportClear){
    btnImportClear.addEventListener('click', async ()=>{
      ImportManager.clear(); SpriteEngine.clear(); await resetRound();
      toast('皮肤缓存已清空（已切换人形回退）');
    });
  }

  // 启动：自动导入种子后加载
  (async function init(){
      // 直接应用内置的两张人物形象（无需拖入/联网）
      ImportManager.clear();
      // 使用内置像素立绘作为参考图，并自动生成基础雪碧图
      try{
        AutoImport.seedOnce();
        const ck = SpriteEngine.cfg.kyo; const co = SpriteEngine.cfg.orochi;
        if(ImportManager.get('kyo_idle')){
          let cut = await AutoImport.removeBgSmart(ImportManager.get('kyo_idle'));
          try{ cut = await AutoImport.augmentLegsIfNeededKyo(cut); }catch(e){}
          const gen = await AutoImport.generateSheetFromImage(cut, ck.frameW, ck.frameH, ck.cols, ck.rows);
          ImportManager.set('kyo_idle', cut); ImportManager.set('kyo_sheet', gen);
        }
        if(ImportManager.get('orochi_idle')){
          const cutO = await AutoImport.removeBgSmart(ImportManager.get('orochi_idle'));
          const genO = await AutoImport.generateSheetFromImage(cutO, co.frameW, co.frameH, co.cols, co.rows);
          ImportManager.set('orochi_idle', cutO); ImportManager.set('orochi_sheet', genO);
        }
      }catch(e){ /* ignore */ }
      SpriteEngine.clear(); await resetRound();
      requestAnimationFrame(loop);
      toast('已直接应用人物形象（无需拖入）');
   })();

  // 预设链接一键尝试（顺序尝试，成功则应用）
  if(btnImportPresetTry){
    const PRESET = {
      kyo_idle: [
        'https://raw.githubusercontent.com/unknown/kof97-assets/main/kyo_idle.png'
      ],
      kyo_sheet: [
        'https://raw.githubusercontent.com/unknown/kof97-assets/main/kyo_sheet.png'
      ],
      orochi_idle: [
        'https://raw.githubusercontent.com/unknown/kof97-assets/main/orochi_idle.png'
      ],
      orochi_sheet: [
        'https://raw.githubusercontent.com/unknown/kof97-assets/main/orochi_sheet.png'
      ]
    };
    async function tryList(key, urls){
      const online = navigator.onLine !== false;
      const withTimeout = (p, ms=1500) => new Promise((resolve, reject)=>{
        let done=false; const timer=setTimeout(()=>{ if(!done){ done=true; reject(new Error('timeout')); } }, ms);
        p.then(v=>{ if(!done){ done=true; clearTimeout(timer); resolve(v); } }).catch(e=>{ if(!done){ done=true; clearTimeout(timer); reject(e); } });
      });
      if(!online) return false;
      for(const u of urls){
        try{ const data = await withTimeout(ImportManager.fromUrlSmart(u), 1500); if(data){ ImportManager.set(key, data); toast(`预设成功：${key}`); return true; } }
        catch(e){ /* ignore and continue */ }
      }
      return false;
    }
    btnImportPresetTry.addEventListener('click', async ()=>{
      btnImportPresetTry.disabled = true;
      let ok = 0;
      ok += (await tryList('kyo_idle', PRESET.kyo_idle))?1:0;
      ok += (await tryList('kyo_sheet', PRESET.kyo_sheet))?1:0;
      ok += (await tryList('orochi_idle', PRESET.orochi_idle))?1:0;
      ok += (await tryList('orochi_sheet', PRESET.orochi_sheet))?1:0;
      // 参考图生成基础雪碧图（如需要）
      try{
        if(ImportManager.get('kyo_idle') && !ImportManager.get('kyo_sheet')){ const c = SpriteEngine.cfg.kyo; const gen = await AutoImport.generateSheetFromImage(ImportManager.get('kyo_idle'), c.frameW, c.frameH, c.cols, c.rows); ImportManager.set('kyo_sheet', gen); }
        if(ImportManager.get('orochi_idle') && !ImportManager.get('orochi_sheet')){ const c = SpriteEngine.cfg.orochi; const gen = await AutoImport.generateSheetFromImage(ImportManager.get('orochi_idle'), c.frameW, c.frameH, c.cols, c.rows); ImportManager.set('orochi_sheet', gen); }
      }catch(e){ /* ignore */ }
      SpriteEngine.clear(); await resetRound();
      importModal.style.display='none';
      toast(`预设尝试完成（成功${ok}/4）`);
      setTimeout(()=>{ btnImportPresetTry.disabled=false; }, 600);
    });
  }
})();